Compare commits

22 Commits

Author SHA1 Message Date
b8ad3aea13 Implement cybernetwork background features: Add routes for serving cybernetwork CSS and JavaScript files, update base.html to include the new CSS and initialization script, and enhance style.css with smooth scrolling and background imports. This update improves the visual experience and functionality of the cybernetwork background in the application. 2025-04-27 15:01:38 +02:00
edf3049e42 Remove deprecated files and templates: Delete unused files including deployment scripts, environment configurations, and various HTML templates to streamline the project structure. This cleanup enhances maintainability and reduces clutter in the codebase. 2025-04-27 14:50:20 +02:00
d117978005 Update GPT model to 'gpt-4o-mini' for chat functionality in app.py 2025-04-27 08:03:55 +02:00
48d8463481 Enhance chat interface styling in index.html: Add animations for chat messages and typing indicators, implement smooth scrolling for chat messages, and customize scrollbar appearance. Introduce hover effects for quick query buttons to improve user interaction and visual feedback. 2025-04-27 08:00:53 +02:00
08314ec703 Enhance embedded ChatGPT assistant functionality: Integrate a new chat interface within the index.html template, allowing users to interact with the assistant directly. Update main.js to ensure proper initialization and reference management. Improve user experience with quick query buttons and a responsive chat layout, while maintaining existing functionality in the application. 2025-04-27 08:00:16 +02:00
0bb7d8d0dc Add ChatGPT assistant initialization in main.js: Integrate a new ChatGPTAssistant instance during the MindMap application initialization, ensuring a global reference for enhanced user interaction. This update improves the functionality of the chat feature within the application. 2025-04-27 07:52:23 +02:00
4a28c2c453 Refactor chat_with_assistant function to support messages array input: Enhance the chatbot API by allowing an array of messages for context, extracting system messages, and updating the response format. Maintain backward compatibility with the previous prompt structure while improving error handling for empty inputs. 2025-04-27 07:49:40 +02:00
66d987857a Remove deprecated database management scripts and admin user creation functionality: Delete create_admin.py, fix_db.py, rebuild_db.py, and test_db.py to streamline the project structure and eliminate unused code. Update README.md with installation instructions and management tools for improved user guidance. 2025-04-27 07:46:48 +02:00
d58aba26c2 Refactor OpenAI integration and enhance mindmap UI: Update OpenAI client initialization to use a dedicated class, streamline API key management, and improve loading animations in the mindmap template. Add a modal for adding new thoughts with enhanced user feedback and error handling, while updating the loading overlay for better visual appeal and responsiveness. 2025-04-27 07:43:03 +02:00
8f0a6d4372 Update environment configuration and enhance app functionality: Add detailed comments to the .env file for better clarity, implement a route to reload environment variables dynamically, and ensure the .env file is loaded with force in the app. Remove obsolete build and development scripts to streamline the project structure. Update setup script to create a .env file if it doesn't exist, prompting users to configure necessary values. 2025-04-27 07:28:05 +02:00
5372fe220e Add flash message API and enhance mindmap visualization: Implement a new API endpoint for retrieving flash messages, integrate flash message display in the mindmap visualization, and improve user feedback with dynamic notifications. Update UI elements for better responsiveness and visual appeal, while removing obsolete background image references. 2025-04-27 07:18:32 +02:00
11ab15127c Refactor mindmap visualization and enhance user authentication UI: Implement API calls to load mindmap data dynamically, process hierarchical data into nodes and links, and improve error handling. Update login and registration templates for a modern design with enhanced validation and user experience. Remove obsolete network background images. 2025-04-27 07:08:38 +02:00
0705ecce59 Implement database path configuration and enhance category management: Update database URI to use an absolute path, ensure directory creation for the database, and implement default category creation on initialization. Add new routes for searching thoughts and user account management, while improving the UI with navigation updates for better accessibility. 2025-04-27 07:02:54 +02:00
1c59b0b616 node entfernt 2025-04-27 06:49:59 +02:00
d42c43db50 Enhance footer layout and mindmap functionality: Revamp footer structure with improved grid layout, add social media icons, and implement a newsletter subscription form. Update mindmap template to use SVG background, streamline script loading, and enhance visualization initialization with new event handlers for user interactions. 2025-04-27 06:33:01 +02:00
e46264b201 Fix: network background loading and fallback mechanism: Implement a retry logic for loading the network background image with a maximum of two attempts, first trying the SVG version and then falling back to a JPG if necessary. Add a fallback background drawing function to maintain visual continuity when image loading fails. Update placeholder comment in JPG file to reflect the use of an SVG alternative. 2025-04-27 06:26:10 +02:00
74307ba345 Add user account route and bookmark functionality: Implement '/my-account' route for user bookmarks and personal mindmap, enhance mindmap visualization with bookmark management, and update UI elements for better user experience. 2025-04-26 18:51:53 +01:00
14474c4eab Refactor UI and enhance functionality: Update welcome messages and input placeholders in the chat assistant, implement connection count updates in the mindmap visualization, and change branding from "MindMap" to "Systades" across templates for a cohesive user experience. 2025-04-26 18:40:27 +01:00
4797cc3b72 Optimize network background animation and enhance UI styles: Adjust animation speeds for smoother transitions, implement dark mode support, and improve card and navbar styles with glassmorphism effects. Update HTML structure for better responsiveness and visual appeal. 2025-04-25 23:39:41 +02:00
ab280b55af Update requirements and enhance mindmap UI: Comment out Pillow dependency, add network background script, and implement new styles and animations for improved visual effects in the mindmap template. 2025-04-25 21:30:35 +01:00
84b492d8d2 Update README and enhance application functionality: Add detailed installation instructions, integrate OpenAI GPT for the AI assistant, implement error handling for various HTTP errors, and improve the admin interface with user management features. Refactor mindmap visualization and enhance UI with modern design elements. 2025-04-25 00:30:04 +02:00
b0db3398f2 Erweitere die Anwendung um neue Funktionen: Implementiere eine dauerhafte Sitzung für den Dark Mode, füge Benutzer- und Gedankenbewertung hinzu, verbessere die Benutzeroberfläche und aktualisiere die Datenbankinitialisierung mit Beispielbenutzern und Gedanken. Optimiere die Templates für ein modernes Design und verbessere die Suchfunktionalität. 2025-04-24 18:08:04 +02:00
5315 changed files with 7683 additions and 869284 deletions

View File

@@ -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

View File

@@ -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.)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
View File

@@ -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
View File

@@ -1 +0,0 @@
logs/app.log

View File

@@ -1,8 +0,0 @@
{
"compilerOptions": {
"target": "esnext",
"lib": [
"esnext"
]
}
}

68
.vscode/main.js vendored
View File

@@ -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
}

View File

@@ -1,239 +0,0 @@
# ABSOLUTE DON'TS:
- Verwendung von npm anstelle der Tailwind CDN
- Implementierung von Content Security Policy (CSP) - UNTER KEINEN UMSTÄNDEN!
- Implementierung von Cross-Site Request Forgery (CSRF) Schutz
- Implementierung von Security Headers
- Verwendung von TypeScript anstelle von JavaScript
- Implementierung von OAuth oder anderen externen Authentifizierungsmethoden
# HÄUFIGE FEHLER:
- Verwendung der falschen Datenbank (die korrekte ist: database/systades.db)
- Falsche Pfadangaben bei statischen Dateien
- Vergessen der deutschen Spracheinstellungen in Templates
- Nicht beachten der vorhandenen Projektstruktur
- Falsche Einbindung der Neural Network Background Animation
- Verwendung von englischen Variablennamen in deutschen Funktionen
- Vergessen der Mindmap-Datenstruktur gemäß der Roadmap
# Häufige Fehler und Lösungen
## Datenbankfehler
### Fehler: "no such column: user.password"
**Fehlerbeschreibung:**
```
sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) no such column: user.password
[SQL: SELECT user.id AS user_id, user.username AS user_username, user.email AS user_email, user.password AS user_password, user.created_at AS user_created_at, user.is_active AS user_is_active, user.role AS user_role
FROM user
WHERE user.id = ?]
```
**Ursache:**
Die Spalte `password` fehlt in der Tabelle `user` der SQLite-Datenbank. Dies kann durch eine unvollständige Datenbankinitialisierung oder ein fehlerhaftes Schema-Update verursacht worden sein.
**Lösung:**
1. **Datenbank reparieren mit dem Fix-Skript**
```bash
python fix_user_table.py
```
Dieses Skript:
- Prüft, ob die Tabelle `user` existiert und erstellt sie, falls nicht
- Prüft, ob die Spalte `password` existiert und fügt sie hinzu, falls nicht
2. **Standardbenutzer erstellen**
```bash
python create_default_users.py
```
Dieses Skript:
- Erstellt Standardbenutzer (admin, user), falls keine vorhanden sind
- Setzt Passwörter mit korrektem Hashing
3. **Datenbank testen**
```bash
python test_app.py
```
Dieses Skript überprüft:
- Ob die Datenbank existiert
- Ob die Tabelle `user` korrekt konfiguriert ist
- Ob Benutzer vorhanden sind
## Häufige Ursachen für Datenbankfehler
1. **Inkonsistente Datenbankschemas**
- Unterschiede zwischen dem SQLAlchemy-Modell und der tatsächlichen Datenbankstruktur
- Fehlende Spalten, die in den Modellen definiert sind
2. **Falsche Datenbankinitialisierung**
- Die Datenbank wurde nicht korrekt initialisiert
- Fehler bei der Migration oder dem Schema-Update
3. **Datenbankdatei-Korrumpierung**
- Die SQLite-Datenbankdatei wurde beschädigt
- Lösung: Sicherung wiederherstellen oder Datenbank neu erstellen
## Vorbeugende Maßnahmen
1. **Regelmäßige Backups**
- Tägliche Sicherung der Datenbankdatei
2. **Schema-Validierung**
- Regelmäßige Überprüfung der Datenbankstruktur
- Automatisierte Tests für Datenbankschema
3. **Datenbankmigration**
- Verwenden Sie Flask-Migrate für strukturierte Datenbank-Updates
- Dokumentieren Sie alle Schemaänderungen
## Content Security Policy (CSP)
### Problem: Externe Ressourcen werden nicht geladen
**Fehler:** Externe Ressourcen wie CDNs werden nicht korrekt geladen.
**Lösung:**
1. Stellen Sie sicher, dass die URLs in den Templates korrekt sind:
```html
<link href="https://cdn.tailwindcss.com" rel="stylesheet">
```
2. Überprüfen Sie die Netzwerkverbindung und ob die CDN-Domains erreichbar sind.
3. Verwenden Sie lokale Ressourcen als Alternative:
```html
<link href="{{ url_for('static', filename='css/tailwind.min.css') }}" rel="stylesheet">
```
### Problem: CSP-Fehler (script-src-elem 'none', font-src 'none', etc.)
**Fehler:** Skripte werden aufgrund von CSP-Einschränkungen blockiert.
**Lösung:**
1. WICHTIG: CSP-Einschränkungen NICHT implementieren! Diese sind im Projekt unerwünscht.
2. Falls CSP-Header bereits vorhanden sind, entfernen Sie diese aus der Anwendung:
```python
# Entfernen Sie diesen Code, falls vorhanden:
# @app.after_request
# def add_security_headers(response):
# response.headers['Content-Security-Policy'] = '...'
# return response
```
3. Stellen Sie sicher, dass externe Ressourcen direkt geladen werden:
```html
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="{{ url_for('static', filename='js/alpine.min.js') }}" defer></script>
```
4. Für Inline-Skripte keine Nonce verwenden:
```html
<script>
// Inline-JavaScript-Code ohne Einschränkungen
</script>
```
### Problem: Tailwind CSS CDN wird blockiert
**Fehler:** Tailwind CSS kann nicht von CDN geladen werden.
**Lösung:**
1. Verwenden Sie die lokale Version von Tailwind CSS:
```html
<link href="{{ url_for('static', filename='css/tailwind.min.css') }}" rel="stylesheet">
```
2. Alternativ können Sie die CDN-Version direkt im Template einbinden:
```html
<script src="https://cdn.tailwindcss.com"></script>
```
3. Stellen Sie sicher, dass die Datei `static/css/tailwind.min.css` existiert und aktuell ist.
## Authentifizierung
### Problem: Login funktioniert nicht
**Fehler:** Benutzer kann sich nicht einloggen.
**Lösung:**
1. Standard-Admin-Benutzer erstellen: `python TOOLS.py user:admin`
2. Passwort zurücksetzen: `python TOOLS.py user:reset-pw -u USERNAME -p NEWPASSWORD`
## Neural Network Background
### Problem: Hintergrund-Animation wird nicht angezeigt
**Fehler:** Die Neural Network Animation im Hintergrund erscheint nicht.
**Lösung:**
1. Überprüfen Sie, ob die Datei `static/neural-network-background.js` korrekt eingebunden ist:
```html
<script src="{{ url_for('static', filename='neural-network-background.js') }}"></script>
```
2. Initialisieren Sie die Animation im Template:
```html
<script>
document.addEventListener('DOMContentLoaded', () => {
const background = new NeuralNetworkBackground();
background.initialize();
background.animate();
});
</script>
```
3. Stellen Sie sicher, dass keine CSS-Regeln die Animation überdecken:
```css
#neural-network-background {
z-index: -10;
opacity: 1;
}
```
## Mindmap-Funktionalität
### Problem: Mindmap-Daten werden nicht geladen
**Fehler:** Die dynamische Mindmap zeigt keine Daten an.
**Lösung:**
1. Überprüfen Sie die API-Endpunkte für die Mindmap-Daten:
```python
@app.route('/api/mindmap/nodes', methods=['GET'])
def get_mindmap_nodes():
# Implementierung...
```
2. Stellen Sie sicher, dass die AJAX-Anfragen korrekt implementiert sind:
```javascript
fetch('/api/mindmap/nodes')
.then(response => response.json())
.then(data => {
// Verarbeitung der Mindmap-Daten
});
```
3. Überprüfen Sie die Datenbankeinträge für Mindmap-Knoten und -Verbindungen.
## ChatGPT-Assistent
### Problem: Assistent reagiert nicht auf Eingaben
**Fehler:** Der ChatGPT-Assistent verarbeitet keine Benutzereingaben.
**Lösung:**
1. Überprüfen Sie die Einbindung der JavaScript-Datei:
```html
<script src="{{ url_for('static', filename='js/modules/chatgpt-assistant.js') }}"></script>
```
2. Stellen Sie sicher, dass der Assistent korrekt initialisiert wird:
```javascript
document.addEventListener('DOMContentLoaded', () => {
const assistant = new ChatGPTAssistant();
assistant.initialize();
});
```
3. Überprüfen Sie die API-Endpunkte für die Kommunikation mit dem Assistenten.

View File

@@ -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"]

View File

@@ -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*

View File

@@ -2,7 +2,7 @@
Diese Roadmap beschreibt die geplante Entwicklung der dynamischen, benutzerorientierten Mindmap-Funktionalität für das Systades-Projekt.
## Phase 1: Grundlegendes Datenmodell und Backend (Abgeschlossen)
## Phase 1: Grundlegendes Datenmodell und Backend (Abgeschlossen)
- [x] Entwurf des Datenbankschemas für benutzerorientierte Mindmaps
- [x] Implementierung der Modelle in models.py
@@ -10,54 +10,31 @@ Diese Roadmap beschreibt die geplante Entwicklung der dynamischen, benutzerorien
- [x] Integration mit der bestehenden Benutzerauthentifizierung
- [x] Seed-Daten für die Entwicklung und Tests
## Phase 2: Dynamische Mindmap-Visualisierung (Abgeschlossen ✅)
## Phase 2: Dynamische Mindmap-Visualisierung (Aktuell)
- [x] Anpassung des Frontend-Codes zur Verwendung der DB-Daten anstelle des SVG
- [x] Implementierung von AJAX-Anfragen zum Laden der Mindmap-Daten
- [x] Dynamisches Rendering der Knoten, Verbindungen und Labels
- [x] Drag-and-Drop-Funktionalität für die Bewegung von Knoten
- [x] Zoom- und Pan-Funktionalität mit Persistenz der Ansicht
- [x] Verbesserte Fehlerbehandlung in der Knotenvisualisierung
- [x] Robustere Verbindungserkennung zwischen Knoten
- [x] Implementierung von Glasmorphismus-Effekten für moderneres UI
- [ ] 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: Visuelles Design und UX (Abgeschlossen ✅)
## Phase 3: Benutzerdefinierte Mindmaps
- [x] Implementierung des Dark Mode
- [x] Entwicklung eines modernen, minimalistischen UI
- [x] Animierter neuronaler Netzwerk-Hintergrund mit WebGL
- [x] Responsive Design für alle Geräte
- [x] Verbesserte Hover- und Selektionseffekte
- [x] Clustertopologie für neuronale Netzwerkdarstellung
- [x] Animierte Neuronenfeuer-Simulation mit Signalweiterleitung
## Phase 4: Benutzerdefinierte Mindmaps (Aktuell 🔄)
- [x] UI für das Betrachten bestehender Mindmaps
- [ ] UI für das Erstellen und Bearbeiten eigener Mindmaps
- [ ] 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
## Phase 5: Notizen und Annotationen
## Phase 4: Notizen und Annotationen
- [x] Anzeige von Gedanken zu Mindmap-Knoten
- [ ] UI für das Hinzufügen privater Notizen zu Knoten
- [ ] Visuelle Anzeige von Notizen in der Mindmap
- [ ] Texteditor mit Markdown-Unterstützung für Notizen
- [ ] Kategorisierung und Farbkodierung von Notizen
- [ ] Suchfunktion für Notizen
## Phase 6: Tagging und Quellenmanagement
- [ ] Tagging-System für Inhalte implementieren
- [ ] Verknüpfen von Quellen mit Mindmap-Knoten
- [ ] Upload-Funktionalität für Dateien und Medien
- [ ] Verwaltung von Zitaten und Referenzen
- [ ] Visuelles Feedback für Tags und Quellen in der Mindmap
## Phase 7: Integrationen und Erweiterungen
## Phase 5: Integrationen und Erweiterungen
- [ ] Import/Export-Funktionalität für Mindmaps (JSON, PNG)
- [ ] Teilen von Mindmaps (öffentlich/privat/mit bestimmten Benutzern)
@@ -65,7 +42,7 @@ Diese Roadmap beschreibt die geplante Entwicklung der dynamischen, benutzerorien
- [ ] Verknüpfung mit externen Ressourcen (Links, Dateien)
- [ ] Versionierung von Mindmaps
## Phase 8: KI-Integration und Analyse
## Phase 6: KI-Integration und Analyse
- [ ] KI-gestützte Vorschläge für Verbindungen zwischen Knoten
- [ ] Automatische Kategorisierung von Inhalten
@@ -73,7 +50,7 @@ Diese Roadmap beschreibt die geplante Entwicklung der dynamischen, benutzerorien
- [ ] Mindmap-Statistiken und Analysen
- [ ] KI-basierte Zusammenfassung von Teilbereichen der Mindmap
## Phase 9: Optimierung und Skalierung
## Phase 7: Optimierung und Skalierung
- [ ] Performance-Optimierung für große Mindmaps
- [ ] Verbesserung der Benutzerfreundlichkeit basierend auf Feedback
@@ -89,20 +66,6 @@ Diese Roadmap beschreibt die geplante Entwicklung der dynamischen, benutzerorien
- [ ] Caching-Strategien für bessere Performance
- [ ] Verbesserte Fehlerbehandlung und Logging
## KI-Integration
### Aktuelle Implementation
- Integration von OpenAI mit dem gpt-4o-mini-Modell für den KI-Assistenten
- Datenbankzugriff für den KI-Assistenten, um direkt Informationen aus der Datenbank abzufragen
- Verbesserte Benutzeroberfläche für den KI-Assistenten mit kontextbezogenen Vorschlägen
### Zukünftige Verbesserungen
- Implementierung von Vektorsuche für präzisere Datenbank-Abfragen durch die KI
- Erweiterung der KI-Funktionalität für tiefere Analyse von Zusammenhängen zwischen Gedanken
- KI-gestützte Vorschläge für neue Verbindungen zwischen Gedanken basierend auf Inhaltsanalyse
- Finetuning des KI-Modells auf die spezifischen Anforderungen der Anwendung
- Erweiterung auf multimodale Fähigkeiten (Bild- und Textanalyse)
---
## Implementierungsdetails
@@ -122,7 +85,6 @@ Das Datenbankschema umfasst folgende Hauptentitäten:
### Frontend-Technologien
- D3.js für die Visualisierung der Mindmap
- WebGL für den neuronalen Netzwerk-Hintergrund
- AJAX für dynamisches Laden von Daten
- Interaktive Bedienelemente mit JavaScript
- Responsive Design mit Tailwind CSS
@@ -138,35 +100,3 @@ Die implementierten API-Endpunkte umfassen:
- `/api/mindmap/<id>/update_node_position` - Aktualisierung von Knotenpositionen
- `/api/mindmap/<id>/notes` - Verwaltung von Notizen
- `/api/nodes/<id>/thoughts` - Abrufen und Hinzufügen von Gedanken zu Knoten
- `/api/get_dark_mode` - Abrufen der Dark Mode Einstellung
## Neuronaler Netzwerk-Hintergrund
Der neue WebGL-basierte Hintergrund bietet:
- WebGL-basierte Rendering-Engine für optimale Performance
- Dynamische Knoten und Verbindungen mit realistischem Verhalten
- Clustering von neuronalen Knoten für natürlicheres Erscheinungsbild
- Simulation von neuronaler Aktivität und Signalweiterleitung
- Anpassbare visuelle Parameter (Helligkeit, Dichte, Geschwindigkeit)
- Vollständig responsives Design für alle Bildschirmgrößen
## Aktuelle Verbesserungen
- Tailwind CSS wurde auf CDN-Version aktualisiert (06.06.2024)
- Content Security Policy (CSP) für Tailwind CSS CDN und WebGL konfiguriert
- Behebung kritischer Fehler in der Mindmap-Knotenvisualisierung (15.06.2024)
- Verbesserte Verbindungserkennung zwischen Knoten implementiert
- Robuste Fehlerbehandlung für verschiedene API-Datenformate
## Zukünftige Aufgaben (Q3 2024)
- Implementierung des Tagging-Systems für Gedanken
- Quellenmanagement für Mindmap-Knoten
- Erweiterte Benutzerprofilfunktionen
- Verbesserung der mobilen Benutzererfahrung
- Integration von Exportfunktionen für Mindmaps
*Zuletzt aktualisiert: 15.06.2024*
## [Entfernt] CORS-Unterstützung (flask-cors)
- Die flask-cors-Bibliothek und alle zugehörigen Initialisierungen wurden entfernt.
- CORS wird nicht mehr unterstützt oder benötigt.

0
TOOLS.py Normal file → Executable file
View File

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.

2107
app.py Normal file → Executable file

File diff suppressed because it is too large Load Diff

2526
app.py.bak

File diff suppressed because it is too large Load Diff

Binary file not shown.

4
cookies.txt Normal file
View File

@@ -0,0 +1,4 @@
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.

Binary file not shown.

Binary file not shown.

View File

@@ -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:

View File

@@ -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

512
init_db.py Normal file → Executable file
View 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_db():
def init_database():
"""Initialisiert die Datenbank mit Beispieldaten."""
with app.app_context():
print("Initialisiere Datenbank...")
# 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)
# 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()
# Admin-Benutzer erstellen
admin = User(username='admin', email='admin@example.com', is_admin=True)
admin.set_password('admin')
db.session.add(admin)
# 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)
# 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()
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"
}
]
# Wissenschaftliche Kategorien erstellen
science = Category(name='Wissenschaft', description='Wissenschaftliche Erkenntnisse',
color_code='#4CAF50', icon='flask')
db.session.add(science)
# 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
philosophy = Category(name='Philosophie', description='Philosophische Theorien und Gedanken',
color_code='#9C27B0', icon='lightbulb')
db.session.add(philosophy)
# 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)
technology = Category(name='Technologie', description='Technologische Entwicklungen',
color_code='#FF9800', icon='microchip')
db.session.add(technology)
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"""
# 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)
# Kategorien für die Zuordnung
categories = Category.query.all()
category_map = {cat.name: cat for cat in categories}
db.session.add_all([physics, biology, chemistry])
# 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
# 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
)
}
]
# 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
for node in nodes.values():
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.")
# 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():
"""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:")

File diff suppressed because it is too large Load Diff

View File

@@ -1 +0,0 @@
Single-database configuration for Flask.

View File

@@ -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

View File

@@ -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()

View File

@@ -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"}

View File

@@ -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')

View File

@@ -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 ###

View File

@@ -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 ###

187
models.py Normal file → Executable file
View 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,45 +43,30 @@ user_thought_bookmark = db.Table('user_thought_bookmark',
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
last_login = db.Column(db.DateTime)
avatar = db.Column(db.String(200))
bio = db.Column(db.Text)
# Relationships
threads = db.relationship('Thread', backref='creator', lazy=True)
messages = db.relationship('Message', backref='author', lazy=True)
projects = db.relationship('Project', backref='owner', lazy=True)
mindmaps = db.relationship('UserMindmap', backref='user', lazy=True)
# Beziehungen
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'))
def __repr__(self):
return f'<User {self.username}>'
backref=db.backref('bookmarked_by', lazy='dynamic'))
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'
return check_password_hash(self.password_hash, password)
class Category(db.Model):
"""Wissenschaftliche Kategorien für die Gliederung der öffentlichen Mindmap"""
@@ -98,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)
@@ -113,8 +93,6 @@ class MindMapNode(db.Model):
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',
@@ -133,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)
@@ -264,128 +228,3 @@ class Comment(db.Model):
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}>'

View File

@@ -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

View File

@@ -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 "----------------------------------------"

108
static/background.js Normal file
View 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
View File

@@ -0,0 +1 @@
C:\Users\firem\Downloads\background.mp4

View File

@@ -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;
}

View File

@@ -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%;
@@ -216,37 +102,3 @@ 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

View File

@@ -1,363 +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);
}

View File

@@ -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;
}

View 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;
}
}

View File

@@ -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');

View File

@@ -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');

View File

@@ -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

View File

@@ -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');
}

View File

@@ -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');
}

View File

@@ -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

View File

@@ -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()
# SVG zu PNG konvertieren
cairosvg.svg2png(url=svg_path, write_to=png_path, output_width=512, output_height=512)
# Höchste Auflösung für Zwischenspeicherung
max_size = max(sizes)
# PNG zu ICO konvertieren
img = Image.open(png_path)
img.save(ico_path, sizes=[(16, 16), (32, 32), (48, 48), (64, 64), (128, 128)])
# 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)
print(f"Favicon erfolgreich erstellt: {ico_path}")
# 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!")
# Ursprüngliches Favicon konvertieren
svg_to_ico(
os.path.join(CURRENT_DIR, 'favicon.svg'),
os.path.join(CURRENT_DIR, 'favicon.ico')
)
# Neues Neuron-Favicon konvertieren
svg_to_ico(
os.path.join(CURRENT_DIR, 'neuron-favicon.svg'),
os.path.join(CURRENT_DIR, 'neuron-favicon.ico')
)
# Optional: PNG-Datei löschen, wenn nur ICO benötigt wird
# os.remove(png_path)

View File

@@ -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

View File

@@ -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

File diff suppressed because one or more lines are too long

View File

@@ -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;
}
// 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 = {
});
}
};
// Globale Export für andere Module
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);
});

View File

@@ -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 = '';
}
}

View File

@@ -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,24 +100,17 @@ class ChatGPTAssistant {
setupEventListeners() {
// Toggle-Button
const toggleButton = document.getElementById('assistant-toggle');
if (toggleButton) {
toggleButton.addEventListener('click', () => this.toggleAssistant());
}
// Schließen-Button
const closeButton = document.getElementById('assistant-close');
if (closeButton) {
closeButton.addEventListener('click', () => this.toggleAssistant(false));
}
// Senden-Button
const sendButton = document.getElementById('assistant-send');
if (sendButton) {
sendButton.addEventListener('click', () => this.sendMessage());
}
// Enter-Taste im Eingabefeld
if (this.inputField) {
this.inputField.addEventListener('keyup', (e) => {
if (e.key === 'Enter') {
this.sendMessage();
@@ -196,38 +118,20 @@ class ChatGPTAssistant {
});
}
// Vorschläge klickbar machen
if (this.suggestionArea) {
this.suggestionArea.addEventListener('click', (e) => {
if (e.target.classList.contains('suggestion-pill')) {
this.inputField.value = e.target.textContent;
this.sendMessage();
}
});
}
}
/**
* Öffnet oder schließt den Assistenten
* @param {boolean} state - Optional: erzwingt einen bestimmten Zustand
*/
toggleAssistant(state = null) {
const chatContainer = document.getElementById('assistant-chat');
if (!chatContainer) return;
this.isOpen = state !== null ? state : !this.isOpen;
if (this.isOpen) {
chatContainer.classList.remove('max-h-0', 'opacity-0');
chatContainer.classList.add('max-h-[32rem]', 'opacity-100');
if (this.inputField) this.inputField.focus();
// Zeige Vorschläge wenn verfügbar
if (this.suggestionArea && this.suggestionArea.children.length > 0) {
this.suggestionArea.classList.remove('hidden');
}
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 {
? '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;
}
// Links in der Nachricht klickbar machen
const links = bubble.querySelectorAll('a');
links.forEach(link => {
link.target = '_blank';
link.rel = 'noopener noreferrer';
link.className = 'text-primary-600 dark:text-primary-400 underline';
});
// Code-Blöcke stylen
const codeBlocks = bubble.querySelectorAll('pre');
codeBlocks.forEach(block => {
block.className = 'bg-gray-100 dark:bg-dark-900 p-2 rounded my-2 overflow-x-auto';
});
const inlineCode = bubble.querySelectorAll('code:not(pre code)');
inlineCode.forEach(code => {
code.className = 'bg-gray-100 dark:bg-dark-900 px-1 rounded font-mono text-sm';
});
messageEl.appendChild(bubble);
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');
}
} 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;

View 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;
}

View 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

View 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;

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

File diff suppressed because it is too large Load Diff

View 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);
});

View 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

View File

@@ -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;
}
});
});

View File

@@ -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

File diff suppressed because one or more lines are too long

100
tailwind.config.js Normal file
View 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
],
}

View File

@@ -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 %}

View File

@@ -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,249 +81,35 @@
<!-- 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">
<!-- Network Background Script -->
<script src="{{ url_for('static', filename='network-background.js') }}"></script>
<!-- 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>
<!-- 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>
<!-- Seitenspezifische Styles -->
{% block extra_css %}{% endblock %}
<!-- Custom dark/light mode styles -->
<!-- ► ► FarbToken strikt getrennt ◄ ◄ -->
<style>
/* LightMode */
: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;
}
/* DarkMode */
.dark {
--bg-primary:#181c24;
--bg-secondary:#232837;
--text-primary:#f9fafb;
--text-secondary:#e5e7eb;
--accent-primary:#6d28d9;
--accent-secondary:#8b5cf6;
--glow-effect:0 0 8px rgba(124,58,237,.15);
}
body {
@apply min-h-screen bg-[color:var(--bg-primary)] text-[color:var(--text-primary)];
transition: background-color 0.5s ease-in-out, color 0.3s ease-in-out, background-image 0.5s ease-in-out;
}
/* Utilities */
.mystical-glow { text-shadow: var(--glow-effect); }
.gradient-text {
background:linear-gradient(135deg,var(--accent-primary),var(--accent-secondary));
-webkit-background-clip:text; background-clip:text; color:transparent; text-shadow:none;
}
.glass-morphism { backdrop-filter: blur(10px); }
.glass-navbar { @apply glass-morphism border backdrop-blur-xl; }
.light .glass-navbar { background-color:rgba(255,255,255,.8); border-color:rgba(0,0,0,.05); }
.dark .glass-navbar { background-color:rgba(10,14,25,.8); border-color:rgba(255,255,255,.05); }
/* Light-Mode spezifische Stile */
body:not(.dark) {
background-color: var(--bg-primary);
color: var(--text-primary);
}
.nav-link-light {
color: var(--text-secondary);
transition: all 0.3s ease;
}
.nav-link-light:hover {
color: var(--text-primary);
background-color: rgba(126, 34, 206, 0.1);
}
.nav-link-light-active {
color: var(--accent-primary);
background-color: rgba(126, 34, 206, 0.15);
font-weight: 500;
}
/* Kartendesign im Light-Mode */
body:not(.dark) .card {
background-color: white;
border: 1px solid #e5e7eb;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
body:not(.dark) .card:hover {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
a:hover {
color: var(--light-primary-hover);
}
/* Light Mode Buttons */
body:not(.dark) .btn,
body:not(.dark) button:not(.toggle) {
background: linear-gradient(135deg, #7c3aed, #6d28d9);
color: white !important;
border: none;
box-shadow: 0 2px 4px rgba(124, 58, 237, 0.25);
border-radius: 8px;
padding: 0.625rem 1.25rem;
transition: all 0.2s ease;
font-weight: 600;
letter-spacing: 0.02em;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
body:not(.dark) .btn:hover,
body:not(.dark) button:not(.toggle):hover {
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(124, 58, 237, 0.3);
color: white !important;
}
/* KI-Chat Button im Light-Mode */
body:not(.dark) [onclick*="MindMap.assistant.toggleAssistant"] {
background: linear-gradient(135deg, #7c3aed, #4f46e5);
color: white !important;
font-weight: 500;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
body:not(.dark) [onclick*="MindMap.assistant.toggleAssistant"]:hover {
background: linear-gradient(135deg, #8b5cf6, #6366f1);
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
}
/* Style improvements for the theme toggle button */
.theme-toggle {
position: relative;
width: 48px;
height: 24px;
border-radius: 24px;
padding: 2px;
cursor: pointer;
transition: all 0.3s ease;
overflow: hidden;
}
body.dark .theme-toggle {
background: linear-gradient(to right, #7c3aed, #3b82f6);
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3), 0 0 10px rgba(124, 58, 237, 0.3);
border: 1px solid rgba(255, 255, 255, 0.1);
}
body:not(.dark) .theme-toggle {
background: linear-gradient(to right, #8b5cf6, #60a5fa);
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1), 0 0 10px rgba(124, 58, 237, 0.15);
border: 1px solid rgba(0, 0, 0, 0.05);
}
.theme-toggle::after {
content: '';
position: absolute;
width: 20px;
height: 20px;
border-radius: 50%;
top: 2px;
transition: all 0.3s ease;
z-index: 2;
}
body.dark .theme-toggle::after {
background: #f1f5f9 url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%237c3aed' width='14' height='14'%3E%3Cpath d='M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z'%3E%3C/path%3E%3C/svg%3E") no-repeat center center;
transform: translateX(24px);
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
}
body:not(.dark) .theme-toggle::after {
background: #fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23f59e0b' width='14' height='14'%3E%3Cpath d='M12 2.25a.75.75 0 01.75.75v2.25a.75.75 0 01-1.5 0V3a.75.75 0 01.75-.75zM7.5 12a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM18.894 6.166a.75.75 0 00-1.06-1.06l-1.591 1.59a.75.75 0 101.06 1.061l1.591-1.59zM21.75 12a.75.75 0 01-.75.75h-2.25a.75.75 0 010-1.5H21a.75.75 0 01.75.75zM17.834 18.894a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 10-1.061 1.06l1.59 1.591zM12 18a.75.75 0 01.75.75V21a.75.75 0 01-1.5 0v-2.25A.75.75 0 0112 18zM7.758 17.303a.75.75 0 00-1.061-1.06l-1.591 1.59a.75.75 0 001.06 1.061l1.591-1.59zM6 12a.75.75 0 01-.75.75H3a.75.75 0 010-1.5h2.25A.75.75 0 016 12zM6.697 7.757a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 00-1.061 1.06l1.59 1.591z'%3E%3C/path%3E%3C/svg%3E") no-repeat center center;
transform: translateX(2px);
box-shadow: 0 0 8px rgba(124, 58, 237, 0.2);
}
.theme-toggle:hover::after {
box-shadow: 0 0 12px rgba(124, 58, 237, 0.4);
}
/* Fixes for light mode button text colors */
body:not(.dark) .btn-primary {
color: white !important;
}
/* Fix for KI-Chat container */
#chatgpt-assistant {
position: fixed;
bottom: 1.5rem;
right: 1.5rem;
z-index: 100;
}
.chat-assistant {
max-height: 80vh !important;
}
.chat-assistant .chat-messages {
max-height: calc(80vh - 160px) !important;
}
</style>
</head>
<body data-page="{{ request.endpoint }}" class="relative overflow-x-hidden dark bg-gray-900 text-white" x-data="{
darkMode: true,
<!-- 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.initDarkMode();
},
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')
// 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';
this.applyDarkMode();
document.querySelector('html').classList.toggle('dark', this.darkMode);
}
})
.catch(error => {
@@ -344,18 +117,12 @@
});
},
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();
document.querySelector('html').classList.toggle('dark', this.darkMode);
// Server über Änderung informieren
fetch('/api/set_dark_mode', {
// Speichere den Dark Mode-Status auf dem Server
fetch('/set_dark_mode', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
@@ -365,26 +132,50 @@
.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 %}
<!-- Cybertechnisches Netzwerk-Hintergrund -->
<script type="module" src="{{ url_for('static', filename='js/modules/cyber-network-init.js') }}"></script>
</head>
<body data-page="{{ request.endpoint }}" class="relative overflow-x-hidden">
<!-- Cybertechnisches Netzwerk-Hintergrund Container (wird via JavaScript befüllt) -->
<div id="cyber-background-container" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: -1; pointer-events: none; overflow: hidden;"></div>
<!-- Globaler Hintergrund -->
<div class="full-page-bg"></div>
<!-- Statischer Fallback-Hintergrund (wird nur angezeigt, wenn JavaScript deaktiviert ist) -->
<div class="fixed inset-0 z-[-9] bg-cover bg-center opacity-50"></div>
<!-- App-Container -->
<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>
@@ -415,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 %}
@@ -432,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 }">
@@ -453,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ü -->
@@ -515,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"
class="flex items-center px-4 py-2.5 rounded-xl font-medium 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
? '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>
<a href="{{ url_for('register') }}"
class="py-2 px-4 rounded-lg transition-all duration-300 font-medium"
x-bind:class="darkMode
? 'bg-purple-800/80 text-white hover:bg-purple-700/80'
: 'bg-purple-600/20 text-gray-700 hover:bg-purple-600/30'">
Registrieren
</a>
</div>
{% endif %}
<!-- Mobilmenü-Button -->
@@ -547,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"
@@ -585,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 %}
@@ -651,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'">
@@ -683,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
@@ -734,192 +509,13 @@
<!-- 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') }}";
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;
}
}
// 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) {
@@ -934,86 +530,6 @@
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>

View File

@@ -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 %}

View File

@@ -1,344 +0,0 @@
{% extends 'base.html' %}
{% block title %}Beitrag bearbeiten{% endblock %}
{% block extra_css %}
<style>
.markdown-preview {
min-height: 200px;
max-height: 400px;
overflow-y: auto;
line-height: 1.6;
}
.markdown-preview p {
margin-bottom: 1rem;
}
.markdown-preview h1, .markdown-preview h2, .markdown-preview h3,
.markdown-preview h4, .markdown-preview h5, .markdown-preview h6 {
margin-top: 1.5rem;
margin-bottom: 0.75rem;
font-weight: 600;
}
.markdown-preview h1 { font-size: 1.8rem; }
.markdown-preview h2 { font-size: 1.5rem; }
.markdown-preview h3 { font-size: 1.3rem; }
.markdown-preview h4 { font-size: 1.1rem; }
.markdown-preview ul, .markdown-preview ol {
margin-left: 1.5rem;
margin-bottom: 1rem;
}
.markdown-preview ul { list-style-type: disc; }
.markdown-preview ol { list-style-type: decimal; }
.markdown-preview pre {
background-color: rgba(0,0,0,0.05);
padding: 1rem;
border-radius: 0.5rem;
overflow-x: auto;
margin: 1rem 0;
}
.markdown-preview code {
font-family: 'JetBrains Mono', monospace;
font-size: 0.9em;
padding: 0.1em 0.3em;
border-radius: 0.3em;
background-color: rgba(0,0,0,0.05);
}
.markdown-preview pre code {
padding: 0;
background-color: transparent;
}
.markdown-preview blockquote {
border-left: 4px solid;
padding-left: 1rem;
margin-left: 0;
margin-right: 0;
margin-bottom: 1rem;
opacity: 0.8;
}
.dark .markdown-preview code {
background-color: rgba(255,255,255,0.1);
}
.dark .markdown-preview blockquote {
border-color: rgba(255,255,255,0.2);
}
.node-mention {
display: inline-flex;
align-items: center;
background-color: rgba(109, 40, 217, 0.1);
color: #6d28d9;
border-radius: 4px;
padding: 1px 6px;
font-size: 0.9em;
margin: 0 2px;
font-weight: 500;
}
.dark .node-mention {
background-color: rgba(167, 139, 250, 0.2);
color: #a78bfa;
}
</style>
{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8">
<!-- Breadcrumb Navigation -->
<div class="mb-6 flex items-center text-sm">
<a href="{{ url_for('community') }}" class="opacity-75 hover:opacity-100 transition-opacity">
<i class="fas fa-home mr-1"></i> Forum
</a>
<span class="mx-2 opacity-50">/</span>
<a href="{{ url_for('forum_category', category_id=post.category_id) }}" class="opacity-75 hover:opacity-100 transition-opacity">
{{ post.category.title }}
</a>
<span class="mx-2 opacity-50">/</span>
{% if post.parent_id %}
<a href="{{ url_for('forum_post', post_id=post.parent_id) }}" class="opacity-75 hover:opacity-100 transition-opacity truncate max-w-[200px]">
{{ post.parent.title }}
</a>
<span class="mx-2 opacity-50">/</span>
{% endif %}
<span class="font-medium">Beitrag bearbeiten</span>
</div>
<!-- Formular-Header -->
<div class="mb-6">
<h1 class="text-2xl font-bold mb-2">Beitrag bearbeiten</h1>
<p class="opacity-75">
{% if post.parent_id %}
Antwort auf <span class="font-medium">{{ post.parent.title }}</span>
{% else %}
in der Kategorie <span class="font-medium">{{ post.category.title }}</span>
{% endif %}
</p>
</div>
<!-- Formular -->
<div class="mb-8 rounded-xl overflow-hidden"
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200'">
<div class="p-4 border-b font-medium" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
<i class="fas fa-edit mr-2"></i>
Beitrag bearbeiten
</div>
<div class="p-6">
<form action="{{ url_for('edit_post', post_id=post.id) }}" method="POST" x-data="{
title: '{{ post.title|safe }}',
content: '{{ post.content|replace('\n', '\\n')|replace('\'', '\\\'')|safe }}',
showPreview: false,
previewHtml: '',
updatePreview() {
// Verarbeite den Inhalt
if (this.content.trim() === '') {
this.previewHtml = '<div class=\'opacity-50 italic\'>Die Vorschau wird hier angezeigt...</div>';
return;
}
// Verarbeite Markdown
let html = marked.parse(this.content);
// Ersetze @Knotenname mit entsprechenden Links
html = html.replace(/@([a-zA-Z0-9äöüÄÖÜß_-]+)/g, '<span class=\'node-mention\'><i class=\'fas fa-diagram-project fa-xs mr-1\'></i>$1</span>');
this.previewHtml = html;
}
}">
<div class="mb-6">
<label for="title" class="block mb-2 font-medium">Titel</label>
<div class="rounded-lg overflow-hidden"
x-bind:class="darkMode ? 'border border-white/20' : 'border border-gray-300'">
<input type="text" id="title" name="title"
class="w-full px-4 py-3"
x-bind:class="darkMode
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:border-indigo-500'
: 'bg-white text-gray-700 placeholder-gray-400 focus:border-indigo-500'"
x-model="title"
required>
</div>
</div>
<div class="mb-6">
<div class="flex justify-between items-center mb-2">
<label for="content" class="font-medium">Inhalt</label>
<div class="flex space-x-2">
<button type="button"
class="px-3 py-1 rounded text-sm flex items-center"
x-bind:class="darkMode
? 'bg-gray-700 hover:bg-gray-600 text-white'
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'"
@click="showPreview = false"
x-bind:disabled="!showPreview"
x-bind:class="{'opacity-50': !showPreview}">
<i class="fas fa-edit mr-1"></i> Bearbeiten
</button>
<button type="button"
class="px-3 py-1 rounded text-sm flex items-center"
x-bind:class="darkMode
? 'bg-gray-700 hover:bg-gray-600 text-white'
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'"
@click="updatePreview(); showPreview = true"
x-bind:disabled="showPreview"
x-bind:class="{'opacity-50': showPreview}">
<i class="fas fa-eye mr-1"></i> Vorschau
</button>
</div>
</div>
<!-- Editor -->
<div class="rounded-lg overflow-hidden mb-2"
x-bind:class="darkMode ? 'border border-white/20' : 'border border-gray-300'"
x-show="!showPreview">
<textarea id="content" name="content" rows="12"
class="w-full p-3 resize-y"
x-bind:class="darkMode
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:border-indigo-500'
: 'bg-white text-gray-700 placeholder-gray-400 focus:border-indigo-500'"
x-model="content"
required></textarea>
</div>
<!-- Preview -->
<div class="rounded-lg overflow-hidden mb-2 p-4 markdown-preview"
x-bind:class="darkMode
? 'border border-white/20 bg-gray-700/30'
: 'border border-gray-300 bg-gray-50'"
x-show="showPreview"
x-html="previewHtml">
</div>
<!-- Markdown-Hilfsmittel -->
<div class="mb-4" x-show="!showPreview">
<div class="text-xs opacity-70">
<p>Du kannst Knotenpunkte der Mindmap durch <code>@Knotenname</code> verlinken.</p>
<p>Dieser Editor unterstützt Markdown-Formatierung:</p>
<div class="flex flex-wrap gap-2 mt-1">
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="**" data-before="" data-after="" title="Fett">
<i class="fas fa-bold"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="*" data-before="" data-after="" title="Kursiv">
<i class="fas fa-italic"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="`" data-before="" data-after="" title="Code">
<i class="fas fa-code"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="[Link-Text](URL)" data-before="" data-after="" title="Link">
<i class="fas fa-link"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="\n```\nCode-Block\n```" data-before="" data-after="" title="Code-Block">
<i class="fas fa-file-code"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format=">" data-before="" data-after="" title="Zitat">
<i class="fas fa-quote-right"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="- " data-before="" data-after="" title="Liste">
<i class="fas fa-list-ul"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="1. " data-before="" data-after="" title="Nummerierte Liste">
<i class="fas fa-list-ol"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="# " data-before="" data-after="" title="Überschrift">
<i class="fas fa-heading"></i>
</button>
</div>
</div>
</div>
</div>
<div class="flex justify-between items-center">
<a href="{{ url_for('forum_post', post_id=post.parent_id or post.id) }}"
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
x-bind:class="darkMode
? 'bg-gray-700 hover:bg-gray-600 text-white'
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'">
Abbrechen
</a>
<button type="submit"
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
x-bind:class="darkMode
? 'bg-indigo-700 hover:bg-indigo-600 text-white'
: 'bg-indigo-500 hover:bg-indigo-600 text-white'">
<i class="fas fa-save mr-2"></i>
Änderungen speichern
</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Markdown-Buttons für den Beitragseditor
document.querySelectorAll('.markdown-button').forEach(button => {
button.addEventListener('click', function() {
const textarea = document.getElementById('content');
const format = this.dataset.format;
const before = this.dataset.before || '';
const after = this.dataset.after || '';
// Hole die aktuelle Auswahl
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const selection = textarea.value.substring(start, end);
// Wende die Formatierung an
let formattedText;
if (format.includes('\n')) {
// Für Formate mit Zeilenumbrüchen (z.B. Code-Blöcke)
formattedText = format.replace('Code-Block', selection || 'Code-Block');
} else if (format.includes('[Link-Text](URL)')) {
formattedText = format.replace('Link-Text', selection || 'Link-Text');
} else if (format === '- ' || format === '1. ' || format === '# ' || format === '> ') {
// Für Listen und Überschriften: am Anfang der Zeile einfügen
const beforeSelection = textarea.value.substring(0, start);
const afterSelection = textarea.value.substring(end);
// Finde den Anfang der aktuellen Zeile
const lastNewline = beforeSelection.lastIndexOf('\n');
const lineStart = lastNewline === -1 ? 0 : lastNewline + 1;
// Füge das Format am Zeilenanfang ein
formattedText = beforeSelection.substring(0, lineStart) +
format +
beforeSelection.substring(lineStart) +
selection +
afterSelection;
// Setze die neue Cursor-Position
const newCursorPos = end + format.length;
textarea.value = formattedText;
textarea.setSelectionRange(newCursorPos, newCursorPos);
// Alpine.js Model aktualisieren
textarea.dispatchEvent(new Event('input'));
return; // Früher zurückkehren, da wir die Formatierung bereits angewendet haben
} else {
// Für einfache Formatierungen wie fett, kursiv, Code
formattedText = before + format + selection + format + after;
}
// Ersetze den Text
textarea.value = textarea.value.substring(0, start) + formattedText + textarea.value.substring(end);
// Setze den Fokus zurück auf das Textarea
textarea.focus();
// Alpine.js Model aktualisieren
textarea.dispatchEvent(new Event('input'));
// Setze die Auswahl neu, wenn es eine Auswahl gab
if (selection) {
const newStart = start + before.length + format.length;
const newEnd = newStart + selection.length;
textarea.setSelectionRange(newStart, newEnd);
} else {
// Setze den Cursor in die Mitte von **|** oder `|`
const newCursorPos = start + before.length + format.length;
textarea.setSelectionRange(newCursorPos, newCursorPos);
}
});
});
});
</script>
{% endblock %}

View File

@@ -1,125 +0,0 @@
{% extends 'base.html' %}
{% block title %}Community Forum{% endblock %}
{% block extra_css %}
<style>
.forum-category {
transition: all 0.3s ease;
}
.forum-category:hover {
transform: translateY(-2px);
}
.category-icon {
font-size: 1.5rem;
width: 2.5rem;
height: 2.5rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 0.75rem;
}
</style>
{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8">
<!-- Seitenüberschrift -->
<div class="mb-8 text-center">
<h1 class="text-3xl font-bold mb-2 gradient-text">Community Forum</h1>
<p class="text-lg opacity-75">Diskutiere mit anderen Nutzern über die Hauptthemenbereiche der Mindmap</p>
</div>
<!-- Forumskategorien -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
{% if categories_data %}
{% for cat_data in categories_data %}
<a href="{{ url_for('forum_category', category_id=cat_data.category.id) }}" class="forum-category block">
<div class="rounded-xl p-5 h-full"
x-bind:class="darkMode ? 'bg-gray-800/60 hover:bg-gray-800/80 border border-white/10' : 'bg-white hover:bg-gray-50 border border-gray-200 shadow-md'">
<div class="flex items-start">
<!-- Kategorie-Icon -->
<div class="category-icon mr-4 text-white"
style="background-color: {{ cat_data.category.node.color_code or '#6d28d9' }}">
<i class="fas {{ cat_data.category.node.icon or 'fa-folder' }}"></i>
</div>
<!-- Kategorie-Info -->
<div class="flex-grow">
<h3 class="text-xl font-semibold mb-2">{{ cat_data.category.title }}</h3>
<p class="opacity-75 text-sm mb-3">{{ cat_data.category.description }}</p>
<!-- Statistik -->
<div class="flex flex-wrap gap-4 text-sm opacity-80">
<div class="flex items-center">
<i class="fas fa-comment-alt mr-2"></i>
<span>{{ cat_data.total_posts }} Themen</span>
</div>
<div class="flex items-center">
<i class="fas fa-reply mr-2"></i>
<span>{{ cat_data.total_replies }} Antworten</span>
</div>
{% if cat_data.latest_post %}
<div class="flex items-center">
<i class="fas fa-clock mr-2"></i>
<span>Neuster Beitrag: {{ cat_data.latest_post.created_at.strftime('%d.%m.%Y, %H:%M') }}</span>
</div>
{% endif %}
</div>
</div>
<!-- Pfeil-Icon -->
<div class="ml-2">
<i class="fas fa-chevron-right opacity-50"></i>
</div>
</div>
</div>
</a>
{% endfor %}
{% else %}
<div class="col-span-2 text-center py-8">
<div class="text-3xl mb-3 opacity-30"><i class="fas fa-exclamation-circle"></i></div>
<h3 class="text-xl font-semibold mb-2">Keine Forum-Kategorien gefunden</h3>
<p class="opacity-75">Es sind derzeit keine Kategorien für Diskussionen verfügbar.</p>
</div>
{% endif %}
</div>
<!-- Hinweis zur Nutzung -->
<div class="rounded-xl p-6 text-center mb-8"
x-bind:class="darkMode ? 'bg-indigo-900/30 border border-indigo-700/30' : 'bg-indigo-50 border border-indigo-100'">
<h3 class="text-xl font-semibold mb-3">
<i class="fas fa-lightbulb mr-2 text-yellow-500"></i>
So funktioniert das Forum
</h3>
<p class="mb-4">Das Community-Forum ist nach den Hauptknotenpunkten der Systades-Mindmap strukturiert.
In deinen Beiträgen kannst du Knotenpunkte mit <code>@Knotenname</code> verlinken.</p>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-6">
<div class="p-4 rounded-lg"
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
<div class="text-2xl mb-2"><i class="fas fa-users text-indigo-400"></i></div>
<h4 class="font-medium mb-1">Fachliche Diskussionen</h4>
<p class="text-sm opacity-75">Tausche dich mit anderen zu spezifischen Themen aus</p>
</div>
<div class="p-4 rounded-lg"
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
<div class="text-2xl mb-2"><i class="fas fa-link text-indigo-400"></i></div>
<h4 class="font-medium mb-1">Wissensvernetzung</h4>
<p class="text-sm opacity-75">Verknüpfe Inhalte durch Knotenreferenzen</p>
</div>
<div class="p-4 rounded-lg"
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
<div class="text-2xl mb-2"><i class="fas fa-markdown text-indigo-400"></i></div>
<h4 class="font-medium mb-1">Markdown Support</h4>
<p class="text-sm opacity-75">Formatiere deine Beiträge mit Markdown</p>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// Hier können bei Bedarf forumspezifische Scripts eingefügt werden
</script>
{% endblock %}

View File

@@ -1,355 +0,0 @@
{% extends 'base.html' %}
{% block title %}Neues Thema - {{ category.title }}{% endblock %}
{% block extra_css %}
<style>
.markdown-preview {
min-height: 200px;
max-height: 400px;
overflow-y: auto;
line-height: 1.6;
}
.markdown-preview p {
margin-bottom: 1rem;
}
.markdown-preview h1, .markdown-preview h2, .markdown-preview h3,
.markdown-preview h4, .markdown-preview h5, .markdown-preview h6 {
margin-top: 1.5rem;
margin-bottom: 0.75rem;
font-weight: 600;
}
.markdown-preview h1 { font-size: 1.8rem; }
.markdown-preview h2 { font-size: 1.5rem; }
.markdown-preview h3 { font-size: 1.3rem; }
.markdown-preview h4 { font-size: 1.1rem; }
.markdown-preview ul, .markdown-preview ol {
margin-left: 1.5rem;
margin-bottom: 1rem;
}
.markdown-preview ul { list-style-type: disc; }
.markdown-preview ol { list-style-type: decimal; }
.markdown-preview pre {
background-color: rgba(0,0,0,0.05);
padding: 1rem;
border-radius: 0.5rem;
overflow-x: auto;
margin: 1rem 0;
}
.markdown-preview code {
font-family: 'JetBrains Mono', monospace;
font-size: 0.9em;
padding: 0.1em 0.3em;
border-radius: 0.3em;
background-color: rgba(0,0,0,0.05);
}
.markdown-preview pre code {
padding: 0;
background-color: transparent;
}
.markdown-preview blockquote {
border-left: 4px solid;
padding-left: 1rem;
margin-left: 0;
margin-right: 0;
margin-bottom: 1rem;
opacity: 0.8;
}
.dark .markdown-preview code {
background-color: rgba(255,255,255,0.1);
}
.dark .markdown-preview blockquote {
border-color: rgba(255,255,255,0.2);
}
.node-mention {
display: inline-flex;
align-items: center;
background-color: rgba(109, 40, 217, 0.1);
color: #6d28d9;
border-radius: 4px;
padding: 1px 6px;
font-size: 0.9em;
margin: 0 2px;
font-weight: 500;
}
.dark .node-mention {
background-color: rgba(167, 139, 250, 0.2);
color: #a78bfa;
}
</style>
{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8">
<!-- Breadcrumb Navigation -->
<div class="mb-6 flex items-center text-sm">
<a href="{{ url_for('community') }}" class="opacity-75 hover:opacity-100 transition-opacity">
<i class="fas fa-home mr-1"></i> Forum
</a>
<span class="mx-2 opacity-50">/</span>
<a href="{{ url_for('forum_category', category_id=category.id) }}" class="opacity-75 hover:opacity-100 transition-opacity">
{{ category.title }}
</a>
<span class="mx-2 opacity-50">/</span>
<span class="font-medium">Neues Thema</span>
</div>
<!-- Formular-Header -->
<div class="mb-6">
<h1 class="text-2xl font-bold mb-2">Neues Thema erstellen</h1>
<p class="opacity-75">in der Kategorie <span class="font-medium">{{ category.title }}</span></p>
</div>
<!-- Formular -->
<div class="mb-8 rounded-xl overflow-hidden"
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200'">
<div class="p-4 border-b font-medium" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
<i class="fas fa-plus-circle mr-2"></i>
Neues Thema
</div>
<div class="p-6">
<form action="{{ url_for('new_post', category_id=category.id) }}" method="POST" x-data="{
title: '',
content: '',
showPreview: false,
previewHtml: '',
updatePreview() {
// Verarbeite den Inhalt
if (this.content.trim() === '') {
this.previewHtml = '<div class=\'opacity-50 italic\'>Die Vorschau wird hier angezeigt...</div>';
return;
}
// Verarbeite Markdown
let html = marked.parse(this.content);
// Ersetze @Knotenname mit entsprechenden Links
html = html.replace(/@([a-zA-Z0-9äöüÄÖÜß_-]+)/g, '<span class=\'node-mention\'><i class=\'fas fa-diagram-project fa-xs mr-1\'></i>$1</span>');
this.previewHtml = html;
}
}">
<div class="mb-6">
<label for="title" class="block mb-2 font-medium">Titel des Themas</label>
<div class="rounded-lg overflow-hidden"
x-bind:class="darkMode ? 'border border-white/20' : 'border border-gray-300'">
<input type="text" id="title" name="title"
class="w-full px-4 py-3"
x-bind:class="darkMode
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:border-indigo-500'
: 'bg-white text-gray-700 placeholder-gray-400 focus:border-indigo-500'"
placeholder="Ein prägnanter Titel für dein Thema"
x-model="title"
required>
</div>
</div>
<div class="mb-6">
<div class="flex justify-between items-center mb-2">
<label for="content" class="font-medium">Inhalt</label>
<div class="flex space-x-2">
<button type="button"
class="px-3 py-1 rounded text-sm flex items-center"
x-bind:class="darkMode
? 'bg-gray-700 hover:bg-gray-600 text-white'
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'"
@click="showPreview = false"
x-bind:disabled="!showPreview"
x-bind:class="{'opacity-50': !showPreview}">
<i class="fas fa-edit mr-1"></i> Bearbeiten
</button>
<button type="button"
class="px-3 py-1 rounded text-sm flex items-center"
x-bind:class="darkMode
? 'bg-gray-700 hover:bg-gray-600 text-white'
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'"
@click="updatePreview(); showPreview = true"
x-bind:disabled="showPreview"
x-bind:class="{'opacity-50': showPreview}">
<i class="fas fa-eye mr-1"></i> Vorschau
</button>
</div>
</div>
<!-- Editor -->
<div class="rounded-lg overflow-hidden mb-2"
x-bind:class="darkMode ? 'border border-white/20' : 'border border-gray-300'"
x-show="!showPreview">
<textarea id="content" name="content" rows="12"
class="w-full p-3 resize-y"
x-bind:class="darkMode
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:border-indigo-500'
: 'bg-white text-gray-700 placeholder-gray-400 focus:border-indigo-500'"
placeholder="Schreibe deinen Beitrag hier (unterstützt Markdown und @Knotenname-Erwähnungen)..."
x-model="content"
required></textarea>
</div>
<!-- Preview -->
<div class="rounded-lg overflow-hidden mb-2 p-4 markdown-preview"
x-bind:class="darkMode
? 'border border-white/20 bg-gray-700/30'
: 'border border-gray-300 bg-gray-50'"
x-show="showPreview"
x-html="previewHtml">
</div>
<!-- Markdown-Hilfsmittel -->
<div class="mb-4" x-show="!showPreview">
<div class="text-xs opacity-70">
<p>Du kannst Knotenpunkte der Mindmap durch <code>@Knotenname</code> verlinken.</p>
<p>Dieser Editor unterstützt Markdown-Formatierung:</p>
<div class="flex flex-wrap gap-2 mt-1">
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="**" data-before="" data-after="" title="Fett">
<i class="fas fa-bold"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="*" data-before="" data-after="" title="Kursiv">
<i class="fas fa-italic"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="`" data-before="" data-after="" title="Code">
<i class="fas fa-code"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="[Link-Text](URL)" data-before="" data-after="" title="Link">
<i class="fas fa-link"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="\n```\nCode-Block\n```" data-before="" data-after="" title="Code-Block">
<i class="fas fa-file-code"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format=">" data-before="" data-after="" title="Zitat">
<i class="fas fa-quote-right"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="- " data-before="" data-after="" title="Liste">
<i class="fas fa-list-ul"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="1. " data-before="" data-after="" title="Nummerierte Liste">
<i class="fas fa-list-ol"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="# " data-before="" data-after="" title="Überschrift">
<i class="fas fa-heading"></i>
</button>
</div>
</div>
</div>
</div>
<div class="flex justify-between items-center">
<a href="{{ url_for('forum_category', category_id=category.id) }}"
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
x-bind:class="darkMode
? 'bg-gray-700 hover:bg-gray-600 text-white'
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'">
Abbrechen
</a>
<button type="submit"
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
x-bind:class="darkMode
? 'bg-indigo-700 hover:bg-indigo-600 text-white'
: 'bg-indigo-500 hover:bg-indigo-600 text-white'">
<i class="fas fa-paper-plane mr-2"></i>
Thema erstellen
</button>
</div>
</form>
</div>
</div>
<!-- Link zur Mindmap -->
<div class="rounded-xl p-5 mb-4 flex items-center"
x-bind:class="darkMode ? 'bg-purple-900/20 border border-purple-800/30' : 'bg-purple-50 border border-purple-100'">
<div class="text-3xl mr-4 opacity-80">
<i class="fas fa-diagram-project" style="color: {{ category.node.color_code }}"></i>
</div>
<div>
<h3 class="font-medium mb-1">Mindmap-Knotenpunkt: {{ category.node.name }}</h3>
<p class="text-sm opacity-75">Dieser Diskussionsbereich ist mit dem Mindmap-Knotenpunkt "{{ category.node.name }}" verknüpft.</p>
</div>
<div class="ml-auto">
<a href="{{ url_for('mindmap') }}"
class="px-4 py-2 rounded-lg inline-block text-sm transition-all"
x-bind:class="darkMode
? 'bg-purple-800/60 hover:bg-purple-700/60 text-white'
: 'bg-white hover:bg-purple-100 text-purple-800 border border-purple-200'">
Zur Mindmap
</a>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Markdown-Buttons für den Beitragseditor
document.querySelectorAll('.markdown-button').forEach(button => {
button.addEventListener('click', function() {
const textarea = document.getElementById('content');
const format = this.dataset.format;
const before = this.dataset.before || '';
const after = this.dataset.after || '';
// Hole die aktuelle Auswahl
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const selection = textarea.value.substring(start, end);
// Wende die Formatierung an
let formattedText;
if (format.includes('\n')) {
// Für Formate mit Zeilenumbrüchen (z.B. Code-Blöcke)
formattedText = format.replace('Code-Block', selection || 'Code-Block');
} else if (format.includes('[Link-Text](URL)')) {
formattedText = format.replace('Link-Text', selection || 'Link-Text');
} else if (format === '- ' || format === '1. ' || format === '# ' || format === '> ') {
// Für Listen und Überschriften: am Anfang der Zeile einfügen
const beforeSelection = textarea.value.substring(0, start);
const afterSelection = textarea.value.substring(end);
// Finde den Anfang der aktuellen Zeile
const lastNewline = beforeSelection.lastIndexOf('\n');
const lineStart = lastNewline === -1 ? 0 : lastNewline + 1;
// Füge das Format am Zeilenanfang ein
formattedText = beforeSelection.substring(0, lineStart) +
format +
beforeSelection.substring(lineStart) +
selection +
afterSelection;
// Setze die neue Cursor-Position
const newCursorPos = end + format.length;
textarea.value = formattedText;
textarea.setSelectionRange(newCursorPos, newCursorPos);
// Alpine.js Model aktualisieren
textarea.dispatchEvent(new Event('input'));
return; // Früher zurückkehren, da wir die Formatierung bereits angewendet haben
} else {
// Für einfache Formatierungen wie fett, kursiv, Code
formattedText = before + format + selection + format + after;
}
// Ersetze den Text
textarea.value = textarea.value.substring(0, start) + formattedText + textarea.value.substring(end);
// Setze den Fokus zurück auf das Textarea
textarea.focus();
// Alpine.js Model aktualisieren
textarea.dispatchEvent(new Event('input'));
// Setze die Auswahl neu, wenn es eine Auswahl gab
if (selection) {
const newStart = start + before.length + format.length;
const newEnd = newStart + selection.length;
textarea.setSelectionRange(newStart, newEnd);
} else {
// Setze den Cursor in die Mitte von **|** oder `|`
const newCursorPos = start + before.length + format.length;
textarea.setSelectionRange(newCursorPos, newCursorPos);
}
});
});
});
</script>
{% endblock %}

View File

@@ -1,511 +0,0 @@
{% extends 'base.html' %}
{% block title %}{{ post.title }} - Forum{% endblock %}
{% block extra_css %}
<style>
.post-content {
line-height: 1.7;
}
.post-content p {
margin-bottom: 1rem;
}
.post-content h1, .post-content h2, .post-content h3,
.post-content h4, .post-content h5, .post-content h6 {
margin-top: 1.5rem;
margin-bottom: 0.75rem;
font-weight: 600;
}
.post-content h1 { font-size: 1.8rem; }
.post-content h2 { font-size: 1.5rem; }
.post-content h3 { font-size: 1.3rem; }
.post-content h4 { font-size: 1.1rem; }
.post-content ul, .post-content ol {
margin-left: 1.5rem;
margin-bottom: 1rem;
}
.post-content ul { list-style-type: disc; }
.post-content ol { list-style-type: decimal; }
.post-content pre {
background-color: rgba(0,0,0,0.05);
padding: 1rem;
border-radius: 0.5rem;
overflow-x: auto;
margin: 1rem 0;
}
.post-content code {
font-family: 'JetBrains Mono', monospace;
font-size: 0.9em;
padding: 0.1em 0.3em;
border-radius: 0.3em;
background-color: rgba(0,0,0,0.05);
}
.post-content pre code {
padding: 0;
background-color: transparent;
}
.post-content blockquote {
border-left: 4px solid;
padding-left: 1rem;
margin-left: 0;
margin-right: 0;
margin-bottom: 1rem;
opacity: 0.8;
}
.post-content img {
max-width: 100%;
height: auto;
border-radius: 0.5rem;
margin: 1rem 0;
}
.post-content table {
width: 100%;
border-collapse: collapse;
margin: 1rem 0;
}
.post-content th, .post-content td {
padding: 0.5rem;
border: 1px solid;
border-color: rgba(0,0,0,0.1);
}
.post-content th {
background-color: rgba(0,0,0,0.05);
}
.post-content a {
color: #6d28d9;
text-decoration: none;
}
.post-content a:hover {
text-decoration: underline;
}
.dark .post-content code {
background-color: rgba(255,255,255,0.1);
}
.dark .post-content th, .dark .post-content td {
border-color: rgba(255,255,255,0.1);
}
.dark .post-content th {
background-color: rgba(255,255,255,0.05);
}
.dark .post-content a {
color: #a78bfa;
}
.node-mention {
display: inline-flex;
align-items: center;
background-color: rgba(109, 40, 217, 0.1);
color: #6d28d9;
border-radius: 4px;
padding: 1px 6px;
font-size: 0.9em;
margin: 0 2px;
font-weight: 500;
}
.dark .node-mention {
background-color: rgba(167, 139, 250, 0.2);
color: #a78bfa;
}
</style>
{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8">
<!-- Breadcrumb Navigation -->
<div class="mb-6 flex items-center text-sm">
<a href="{{ url_for('community') }}" class="opacity-75 hover:opacity-100 transition-opacity">
<i class="fas fa-home mr-1"></i> Forum
</a>
<span class="mx-2 opacity-50">/</span>
<a href="{{ url_for('forum_category', category_id=category.id) }}" class="opacity-75 hover:opacity-100 transition-opacity">
{{ category.title }}
</a>
<span class="mx-2 opacity-50">/</span>
<span class="font-medium truncate max-w-[300px]">{{ post.title }}</span>
</div>
<!-- Beitrags-Header -->
<div class="mb-6">
<h1 class="text-2xl font-bold mb-2">{{ post.title }}</h1>
<div class="flex flex-wrap items-center gap-3 text-sm opacity-75">
<span><i class="fas fa-calendar-alt mr-1"></i> {{ post.created_at.strftime('%d.%m.%Y, %H:%M') }}</span>
<span><i class="fas fa-eye mr-1"></i> {{ post.view_count }} Aufrufe</span>
<span><i class="fas fa-reply mr-1"></i> {{ replies|length }} Antworten</span>
{% if post.is_pinned or post.is_locked %}
<div class="flex gap-2 ml-2">
{% if post.is_pinned %}
<span class="px-2 py-0.5 text-xs rounded-full"
x-bind:class="darkMode ? 'bg-yellow-700/50 text-yellow-300' : 'bg-yellow-100 text-yellow-800'">
<i class="fas fa-thumbtack mr-1"></i> Angepinnt
</span>
{% endif %}
{% if post.is_locked %}
<span class="px-2 py-0.5 text-xs rounded-full"
x-bind:class="darkMode ? 'bg-red-700/50 text-red-300' : 'bg-red-100 text-red-800'">
<i class="fas fa-lock mr-1"></i> Gesperrt
</span>
{% endif %}
</div>
{% endif %}
</div>
</div>
<!-- Hauptbeitrag -->
<div class="mb-8 rounded-xl overflow-hidden"
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200 shadow-sm'">
<!-- Beitrags-Header -->
<div class="p-4 border-b" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
<div class="flex items-center justify-between">
<div class="flex items-center">
<!-- Autor-Avatar -->
<div class="w-10 h-10 rounded-full flex items-center justify-center text-white font-medium text-sm overflow-hidden mr-3"
style="background: linear-gradient(135deg, #8b5cf6, #6366f1);">
{% if post.author.avatar %}
<img src="{{ post.author.avatar }}" alt="{{ post.author.username }}" class="w-full h-full object-cover">
{% else %}
<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(#post-avatar-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="post-avatar-gradient" x1="0" y1="0" x2="200" y2="200" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#8B5CF6"/>
<stop offset="1" stop-color="#3B82F6"/>
</linearGradient>
</defs>
</svg>
{% endif %}
</div>
<!-- Autor-Info -->
<div>
<div class="font-medium">{{ post.author.username }}</div>
<div class="text-xs opacity-70">Erstellt am {{ post.created_at.strftime('%d.%m.%Y, %H:%M') }}</div>
</div>
</div>
<!-- Aktionen -->
<div class="flex items-center space-x-2">
{% if current_user.id == post.user_id or current_user.role == 'admin' %}
<a href="{{ url_for('edit_post', post_id=post.id) }}"
class="p-2 rounded transition-colors"
x-bind:class="darkMode
? 'hover:bg-gray-700/50 text-gray-300'
: 'hover:bg-gray-100 text-gray-600'">
<i class="fas fa-edit"></i>
</a>
<form action="{{ url_for('delete_post', post_id=post.id) }}" method="POST" class="inline" onsubmit="return confirm('Möchtest du diesen Beitrag wirklich löschen?');">
<button type="submit"
class="p-2 rounded transition-colors"
x-bind:class="darkMode
? 'hover:bg-red-800/50 text-red-300'
: 'hover:bg-red-100 text-red-600'">
<i class="fas fa-trash-alt"></i>
</button>
</form>
{% endif %}
<!-- Moderation-Optionen -->
{% if current_user.role in ['admin', 'moderator'] %}
<div class="ml-2 border-l" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'"></div>
<form action="{{ url_for('toggle_pin_post', post_id=post.id) }}" method="POST" class="inline">
<button type="submit"
class="p-2 rounded transition-colors"
x-bind:class="darkMode
? 'hover:bg-yellow-800/50 text-yellow-300'
: 'hover:bg-yellow-100 text-yellow-600'"
title="{% if post.is_pinned %}Nicht mehr anpinnen{% else %}Anpinnen{% endif %}">
<i class="fas fa-thumbtack"></i>
</button>
</form>
<form action="{{ url_for('toggle_lock_post', post_id=post.id) }}" method="POST" class="inline">
<button type="submit"
class="p-2 rounded transition-colors"
x-bind:class="darkMode
? 'hover:bg-blue-800/50 text-blue-300'
: 'hover:bg-blue-100 text-blue-600'"
title="{% if post.is_locked %}Entsperren{% else %}Sperren{% endif %}">
<i class="fas {% if post.is_locked %}fa-unlock{% else %}fa-lock{% endif %}"></i>
</button>
</form>
{% endif %}
</div>
</div>
</div>
<!-- Beitrags-Inhalt -->
<div class="p-6">
<div class="post-content markdown-content" id="main-post-content">
{{ post.content|safe }}
</div>
{% if post.updated_at and post.updated_at != post.created_at %}
<div class="mt-6 pt-4 text-xs opacity-60 border-t" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
<i class="fas fa-edit mr-1"></i> Zuletzt bearbeitet: {{ post.updated_at.strftime('%d.%m.%Y, %H:%M') }}
</div>
{% endif %}
</div>
</div>
<!-- Antworten-Bereich -->
<div class="mb-8">
<h2 class="text-xl font-semibold mb-4">
<i class="fas fa-reply mr-2 opacity-60"></i>
{{ replies|length }} Antworten
</h2>
<!-- Antworten-Liste -->
{% if replies %}
{% for reply in replies %}
<div class="mb-5 rounded-xl overflow-hidden"
x-bind:class="darkMode ? 'bg-gray-800/40 border border-white/10' : 'bg-white border border-gray-200'">
<!-- Antwort-Header -->
<div class="p-3 border-b" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
<div class="flex items-center justify-between">
<div class="flex items-center">
<!-- Autor-Avatar -->
<div class="w-8 h-8 rounded-full flex items-center justify-center text-white font-medium text-xs overflow-hidden mr-3"
style="background: linear-gradient(135deg, #8b5cf6, #6366f1);">
{% if reply.author.avatar %}
<img src="{{ reply.author.avatar }}" alt="{{ reply.author.username }}" class="w-full h-full object-cover">
{% else %}
<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(#reply-avatar-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="reply-avatar-gradient" x1="0" y1="0" x2="200" y2="200" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#8B5CF6"/>
<stop offset="1" stop-color="#3B82F6"/>
</linearGradient>
</defs>
</svg>
{% endif %}
</div>
<!-- Autor-Info -->
<div>
<div class="font-medium text-sm">{{ reply.author.username }}</div>
<div class="text-xs opacity-70">{{ reply.created_at.strftime('%d.%m.%Y, %H:%M') }}</div>
</div>
</div>
<!-- Aktionen -->
<div class="flex items-center space-x-1">
{% if current_user.id == reply.user_id or current_user.role == 'admin' %}
<a href="{{ url_for('edit_post', post_id=reply.id) }}"
class="p-1.5 rounded text-sm transition-colors"
x-bind:class="darkMode
? 'hover:bg-gray-700/50 text-gray-300'
: 'hover:bg-gray-100 text-gray-600'">
<i class="fas fa-edit"></i>
</a>
<form action="{{ url_for('delete_post', post_id=reply.id) }}" method="POST" class="inline" onsubmit="return confirm('Möchtest du diese Antwort wirklich löschen?');">
<button type="submit"
class="p-1.5 rounded text-sm transition-colors"
x-bind:class="darkMode
? 'hover:bg-red-800/50 text-red-300'
: 'hover:bg-red-100 text-red-600'">
<i class="fas fa-trash-alt"></i>
</button>
</form>
{% endif %}
</div>
</div>
</div>
<!-- Antwort-Inhalt -->
<div class="p-5">
<div class="post-content markdown-content reply-content" id="reply-content-{{ reply.id }}">
{{ reply.content|safe }}
</div>
{% if reply.updated_at and reply.updated_at != reply.created_at %}
<div class="mt-4 pt-3 text-xs opacity-60 border-t" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
<i class="fas fa-edit mr-1"></i> Zuletzt bearbeitet: {{ reply.updated_at.strftime('%d.%m.%Y, %H:%M') }}
</div>
{% endif %}
</div>
</div>
{% endfor %}
{% else %}
<div class="rounded-xl p-6 text-center"
x-bind:class="darkMode ? 'bg-gray-800/40 border border-white/10' : 'bg-white border border-gray-200'">
<div class="text-3xl mb-3 opacity-30"><i class="fas fa-comments"></i></div>
<h3 class="text-lg font-semibold mb-2">Noch keine Antworten</h3>
<p class="opacity-75">Sei der Erste, der auf diesen Beitrag antwortet!</p>
</div>
{% endif %}
</div>
<!-- Antwort-Formular -->
{% if not post.is_locked %}
<div class="mb-8 rounded-xl overflow-hidden"
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200'">
<div class="p-4 border-b font-medium" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
<i class="fas fa-reply mr-2"></i>
Antworten
</div>
<div class="p-6">
<form action="{{ url_for('reply_to_post', post_id=post.id) }}" method="POST">
<div class="mb-4">
<label for="content" class="block mb-2 font-medium">Deine Antwort</label>
<div class="mb-2 rounded-lg overflow-hidden"
x-bind:class="darkMode ? 'border border-white/20' : 'border border-gray-300'">
<textarea id="content" name="content" rows="6"
class="w-full p-3 resize-y"
x-bind:class="darkMode
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:border-indigo-500'
: 'bg-white text-gray-700 placeholder-gray-400 focus:border-indigo-500'"
placeholder="Schreibe deine Antwort hier (unterstützt Markdown und @Knotenname-Erwähnungen)..."
required></textarea>
</div>
<div class="text-xs opacity-70">
<p>Du kannst Knotenpunkte der Mindmap durch <code>@Knotenname</code> verlinken.</p>
<p>Dieser Editor unterstützt Markdown-Formatierung:</p>
<div class="flex flex-wrap gap-2 mt-1">
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="**" data-before="" data-after="" title="Fett">
<i class="fas fa-bold"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="*" data-before="" data-after="" title="Kursiv">
<i class="fas fa-italic"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="`" data-before="" data-after="" title="Code">
<i class="fas fa-code"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="[Link-Text](URL)" data-before="" data-after="" title="Link">
<i class="fas fa-link"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="\n```\nCode-Block\n```" data-before="" data-after="" title="Code-Block">
<i class="fas fa-file-code"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format=">" data-before="" data-after="" title="Zitat">
<i class="fas fa-quote-right"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="- " data-before="" data-after="" title="Liste">
<i class="fas fa-list-ul"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="1. " data-before="" data-after="" title="Nummerierte Liste">
<i class="fas fa-list-ol"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="# " data-before="" data-after="" title="Überschrift">
<i class="fas fa-heading"></i>
</button>
</div>
</div>
</div>
<div>
<button type="submit"
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
x-bind:class="darkMode
? 'bg-indigo-700 hover:bg-indigo-600 text-white'
: 'bg-indigo-500 hover:bg-indigo-600 text-white'">
<i class="fas fa-paper-plane mr-2"></i>
Antwort senden
</button>
</div>
</form>
</div>
</div>
{% else %}
<div class="rounded-xl p-5 text-center mb-6"
x-bind:class="darkMode ? 'bg-red-900/20 border border-red-800/30' : 'bg-red-50 border border-red-100'">
<i class="fas fa-lock mr-2 text-red-500"></i>
<span>Dieser Beitrag ist geschlossen. Es können keine neuen Antworten mehr verfasst werden.</span>
</div>
{% endif %}
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Markdown und Knotenerwähnungen verarbeiten
const processContent = (content) => {
// Verarbeite Markdown mit marked.js
let html = marked.parse(content);
// Ersetze @Knotenname mit entsprechenden Links
html = html.replace(/@([a-zA-Z0-9äöüÄÖÜß_-]+)/g, '<span class="node-mention"><i class="fas fa-diagram-project fa-xs mr-1"></i>$1</span>');
return html;
};
// Markdown-Inhalt für Hauptbeitrag rendern
const mainPostContent = document.getElementById('main-post-content');
if (mainPostContent) {
mainPostContent.innerHTML = processContent(mainPostContent.textContent.trim());
}
// Markdown-Inhalt für Antworten rendern
document.querySelectorAll('.reply-content').forEach(reply => {
reply.innerHTML = processContent(reply.textContent.trim());
});
// Markdown-Buttons für das Antwortformular
document.querySelectorAll('.markdown-button').forEach(button => {
button.addEventListener('click', function() {
const textarea = document.getElementById('content');
const format = this.dataset.format;
const before = this.dataset.before || '';
const after = this.dataset.after || '';
// Hole die aktuelle Auswahl
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const selection = textarea.value.substring(start, end);
// Wende die Formatierung an
let formattedText;
if (format.includes('\n')) {
// Für Formate mit Zeilenumbrüchen (z.B. Code-Blöcke)
formattedText = format.replace('Code-Block', selection || 'Code-Block');
} else if (format.includes('[Link-Text](URL)')) {
formattedText = format.replace('Link-Text', selection || 'Link-Text');
} else if (format === '- ' || format === '1. ' || format === '# ' || format === '> ') {
// Für Listen und Überschriften: am Anfang der Zeile einfügen
const beforeSelection = textarea.value.substring(0, start);
const afterSelection = textarea.value.substring(end);
// Finde den Anfang der aktuellen Zeile
const lastNewline = beforeSelection.lastIndexOf('\n');
const lineStart = lastNewline === -1 ? 0 : lastNewline + 1;
// Füge das Format am Zeilenanfang ein
formattedText = beforeSelection.substring(0, lineStart) +
format +
beforeSelection.substring(lineStart) +
selection +
afterSelection;
// Setze die neue Cursor-Position
const newCursorPos = end + format.length;
textarea.value = formattedText;
textarea.setSelectionRange(newCursorPos, newCursorPos);
return; // Früher zurückkehren, da wir die Formatierung bereits angewendet haben
} else {
// Für einfache Formatierungen wie fett, kursiv, Code
formattedText = before + format + selection + format + after;
}
// Ersetze den Text
textarea.value = textarea.value.substring(0, start) + formattedText + textarea.value.substring(end);
// Setze den Fokus zurück auf das Textarea
textarea.focus();
// Setze die Auswahl neu, wenn es eine Auswahl gab
if (selection) {
const newStart = start + before.length + format.length;
const newEnd = newStart + selection.length;
textarea.setSelectionRange(newStart, newEnd);
} else {
// Setze den Cursor in die Mitte von **|** oder `|`
const newCursorPos = start + before.length + format.length;
textarea.setSelectionRange(newCursorPos, newCursorPos);
}
});
});
});
</script>
{% endblock %}

View File

@@ -1,137 +0,0 @@
{% extends 'base.html' %}
{% block title %}Community Forum Vorschau{% endblock %}
{% block extra_css %}
<style>
.forum-category {
transition: all 0.3s ease;
}
.forum-category:hover {
transform: translateY(-2px);
}
.category-icon {
font-size: 1.5rem;
width: 2.5rem;
height: 2.5rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 0.75rem;
}
</style>
{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8">
<!-- Seitenüberschrift -->
<div class="mb-8 text-center">
<h1 class="text-3xl font-bold mb-2 gradient-text">Community Forum</h1>
<p class="text-lg opacity-75">Diskutiere mit anderen Nutzern über die Hauptthemenbereiche der Mindmap</p>
</div>
<!-- Login-Aufforderung -->
<div class="rounded-xl p-6 text-center mb-8 bg-indigo-50 dark:bg-indigo-900/30 border border-indigo-100 dark:border-indigo-700/30">
<h3 class="text-xl font-semibold mb-3">
<i class="fas fa-lock mr-2 text-indigo-500"></i>
Anmeldung erforderlich
</h3>
<p class="mb-4">Um am Community-Forum teilzunehmen und alle Funktionen nutzen zu können, musst du dich anmelden oder registrieren.</p>
<div class="flex justify-center gap-4 mt-4">
<a href="{{ url_for('login', next=url_for('forum')) }}" class="px-4 py-2 bg-indigo-500 hover:bg-indigo-600 text-white rounded-lg transition-colors">
<i class="fas fa-sign-in-alt mr-2"></i>Anmelden
</a>
<a href="{{ url_for('register') }}" class="px-4 py-2 bg-purple-500 hover:bg-purple-600 text-white rounded-lg transition-colors">
<i class="fas fa-user-plus mr-2"></i>Registrieren
</a>
</div>
</div>
<!-- Forumskategorien Vorschau -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
{% if categories_data %}
{% for cat_data in categories_data %}
<div class="forum-category block">
<div class="rounded-xl p-5 h-full"
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200 shadow-md'">
<div class="flex items-start">
<!-- Kategorie-Icon -->
<div class="category-icon mr-4 text-white"
style="background-color: {{ cat_data.category.node.color_code or '#6d28d9' }}">
<i class="fas {{ cat_data.category.node.icon or 'fa-folder' }}"></i>
</div>
<!-- Kategorie-Info -->
<div class="flex-grow">
<h3 class="text-xl font-semibold mb-2">{{ cat_data.category.title }}</h3>
<p class="opacity-75 text-sm mb-3">{{ cat_data.category.description }}</p>
<!-- Statistik -->
<div class="flex flex-wrap gap-4 text-sm opacity-80">
<div class="flex items-center">
<i class="fas fa-comment-alt mr-2"></i>
<span>{{ cat_data.total_posts }} Themen</span>
</div>
<div class="flex items-center">
<i class="fas fa-reply mr-2"></i>
<span>{{ cat_data.total_replies }} Antworten</span>
</div>
</div>
</div>
<!-- Pfeil-Icon -->
<div class="ml-2">
<i class="fas fa-lock opacity-50"></i>
</div>
</div>
</div>
</div>
{% endfor %}
{% else %}
<div class="col-span-2 text-center py-8">
<div class="text-3xl mb-3 opacity-30"><i class="fas fa-exclamation-circle"></i></div>
<h3 class="text-xl font-semibold mb-2">Keine Forum-Kategorien gefunden</h3>
<p class="opacity-75">Es sind derzeit keine Kategorien für Diskussionen verfügbar.</p>
</div>
{% endif %}
</div>
<!-- Hinweis zur Nutzung -->
<div class="rounded-xl p-6 text-center mb-8"
x-bind:class="darkMode ? 'bg-indigo-900/30 border border-indigo-700/30' : 'bg-indigo-50 border border-indigo-100'">
<h3 class="text-xl font-semibold mb-3">
<i class="fas fa-lightbulb mr-2 text-yellow-500"></i>
So funktioniert das Forum
</h3>
<p class="mb-4">Das Community-Forum ist nach den Hauptknotenpunkten der Systades-Mindmap strukturiert.
In deinen Beiträgen kannst du Knotenpunkte mit <code>@Knotenname</code> verlinken.</p>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-6">
<div class="p-4 rounded-lg"
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
<div class="text-2xl mb-2"><i class="fas fa-users text-indigo-400"></i></div>
<h4 class="font-medium mb-1">Fachliche Diskussionen</h4>
<p class="text-sm opacity-75">Tausche dich mit anderen zu spezifischen Themen aus</p>
</div>
<div class="p-4 rounded-lg"
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
<div class="text-2xl mb-2"><i class="fas fa-link text-indigo-400"></i></div>
<h4 class="font-medium mb-1">Wissensvernetzung</h4>
<p class="text-sm opacity-75">Verknüpfe Inhalte durch Knotenreferenzen</p>
</div>
<div class="p-4 rounded-lg"
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
<div class="text-2xl mb-2"><i class="fas fa-markdown text-indigo-400"></i></div>
<h4 class="font-medium mb-1">Markdown Support</h4>
<p class="text-sm opacity-75">Formatiere deine Beiträge mit Markdown</p>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// Hier können bei Bedarf forumspezifische Scripts eingefügt werden
</script>
{% endblock %}

View File

@@ -1,365 +0,0 @@
{% extends "base.html" %}
{% block title %}Mindmap erstellen{% endblock %}
{% block extra_css %}
<style>
/* Spezifische Stile für die Mindmap-Erstellungsseite */
.form-container {
background-color: var(--bg-secondary);
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
body.dark .form-container {
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.05);
}
body:not(.dark) .form-container {
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(0, 0, 0, 0.05);
}
.form-header {
padding: 1.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
body:not(.dark) .form-header {
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
.form-body {
padding: 1.5rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
}
.form-input,
.form-textarea {
width: 100%;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
transition: all 0.3s ease;
}
body.dark .form-input,
body.dark .form-textarea {
background-color: rgba(15, 23, 42, 0.6);
border: 1px solid rgba(255, 255, 255, 0.1);
color: #f1f5f9;
}
body:not(.dark) .form-input,
body:not(.dark) .form-textarea {
background-color: white;
border: 1px solid #e2e8f0;
color: #334155;
}
body.dark .form-input:focus,
body.dark .form-textarea:focus {
border-color: #7c3aed;
box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.2);
outline: none;
}
body:not(.dark) .form-input:focus,
body:not(.dark) .form-textarea:focus {
border-color: #7c3aed;
box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.2);
outline: none;
}
.form-textarea {
min-height: 120px;
resize: vertical;
}
.form-switch {
display: flex;
align-items: center;
}
.form-switch input[type="checkbox"] {
height: 0;
width: 0;
visibility: hidden;
position: absolute;
}
.form-switch label {
cursor: pointer;
width: 50px;
height: 25px;
background: rgba(100, 116, 139, 0.3);
display: block;
border-radius: 25px;
position: relative;
margin-right: 10px;
transition: all 0.3s ease;
}
.form-switch label:after {
content: '';
position: absolute;
top: 3px;
left: 3px;
width: 19px;
height: 19px;
background: #fff;
border-radius: 19px;
transition: 0.3s;
}
.form-switch input:checked + label {
background: #7c3aed;
}
.form-switch input:checked + label:after {
left: calc(100% - 3px);
transform: translateX(-100%);
}
.btn-submit {
background-color: #7c3aed;
color: white;
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
font-weight: 500;
transition: all 0.3s ease;
border: none;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.btn-submit:hover {
background-color: #6d28d9;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(109, 40, 217, 0.2);
}
.btn-cancel {
background-color: transparent;
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
font-weight: 500;
transition: all 0.3s ease;
border: 1px solid;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
body.dark .btn-cancel {
color: #e2e8f0;
border-color: rgba(255, 255, 255, 0.1);
}
body:not(.dark) .btn-cancel {
color: #475569;
border-color: #e2e8f0;
}
.btn-cancel:hover {
transform: translateY(-2px);
}
body.dark .btn-cancel:hover {
background-color: rgba(255, 255, 255, 0.05);
}
body:not(.dark) .btn-cancel:hover {
background-color: rgba(0, 0, 0, 0.05);
}
/* Animation für den Seiteneintritt */
@keyframes slideInUp {
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.form-container {
animation: slideInUp 0.5s ease forwards;
}
/* Animation für Hover-Effekte */
.input-animation {
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.input-animation:focus {
transform: scale(1.01);
}
</style>
{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8 animate-fadeIn">
<div class="max-w-3xl mx-auto">
<!-- Titel mit Animation -->
<div class="text-center mb-8 animate-pulse">
<h1 class="text-3xl font-bold mb-2 mystical-glow gradient-text">
Neue Mindmap erstellen
</h1>
<p class="opacity-80">Erstelle deine eigene Wissenslandkarte und organisiere deine Gedanken</p>
</div>
<div class="form-container">
<div class="form-header">
<h2 class="text-xl font-semibold">Mindmap-Details</h2>
</div>
<div class="form-body">
<form action="{{ url_for('create_mindmap') }}" method="POST">
<div class="form-group">
<label for="name" class="form-label">Name der Mindmap</label>
<input type="text" id="name" name="name" class="form-input input-animation" required placeholder="z.B. Meine Philosophie-Mindmap">
</div>
<div class="form-group">
<label for="description" class="form-label">Beschreibung</label>
<textarea id="description" name="description" class="form-textarea input-animation" placeholder="Worum geht es in dieser Mindmap?"></textarea>
</div>
<div class="form-group">
<div class="form-switch">
<input type="checkbox" id="is_private" name="is_private" checked>
<label for="is_private"></label>
<span>Private Mindmap (nur für dich sichtbar)</span>
</div>
</div>
<div class="flex justify-between mt-6">
<a href="{{ url_for('profile') }}" class="btn-cancel">
<i class="fas fa-arrow-left"></i>
Zurück
</a>
<button type="submit" class="btn-submit">
<i class="fas fa-save"></i>
Mindmap erstellen
</button>
</div>
</form>
<!-- Mindmap-Vorschau -->
<div class="mt-8">
<h3 class="text-xl font-semibold mb-4">Vorschau</h3>
<div class="mindmap-container">
<div id="cy" class="w-full h-[400px] rounded-xl border"
x-bind:class="darkMode ? 'border-gray-700' : 'border-gray-200'">
</div>
</div>
</div>
</div>
</div>
<!-- Tipps-Sektion -->
<div class="mt-8 p-5 rounded-lg border animate-fadeIn"
x-bind:class="darkMode ? 'bg-slate-800/40 border-slate-700/50' : 'bg-white border-slate-200'">
<h3 class="text-xl font-semibold mb-3"
x-bind:class="darkMode ? 'text-white' : 'text-gray-800'">
<i class="fa-solid fa-lightbulb text-yellow-400 mr-2"></i>Tipps zum Erstellen einer Mindmap
</h3>
<div x-bind:class="darkMode ? 'text-gray-300' : 'text-gray-600'">
<ul class="list-disc pl-5 space-y-2">
<li>Wähle einen prägnanten, aber aussagekräftigen Namen für deine Mindmap</li>
<li>Beginne mit einem zentralen Konzept und arbeite dich nach außen vor</li>
<li>Verwende verschiedene Farben für unterschiedliche Kategorien oder Themenbereiche</li>
<li>Füge Notizen zu Knoten hinzu, um komplexere Ideen zu erklären</li>
<li>Verknüpfe verwandte Konzepte, um Beziehungen zu visualisieren</li>
</ul>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.26.0/cytoscape.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape-cose-bilkent/4.1.0/cytoscape-cose-bilkent.min.js"></script>
<script nonce="{{ csp_nonce }}">
document.addEventListener('DOMContentLoaded', function() {
// Einfache Animationen für die Eingabefelder
const inputs = document.querySelectorAll('.input-animation');
inputs.forEach(input => {
// Subtile Skalierung bei Fokus
input.addEventListener('focus', function() {
this.style.transform = 'scale(1.01)';
this.style.boxShadow = '0 4px 12px rgba(124, 58, 237, 0.15)';
});
input.addEventListener('blur', function() {
this.style.transform = 'scale(1)';
this.style.boxShadow = 'none';
});
});
// Formular-Absenden-Animation
const form = document.querySelector('form');
form.addEventListener('submit', function(e) {
const submitBtn = this.querySelector('.btn-submit');
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Wird erstellt...';
submitBtn.disabled = true;
});
// Mindmap-Vorschau initialisieren
const mindmap = new MindMap.Visualization('cy', {
enableEditing: true,
onNodeClick: function(nodeData) {
console.log("Knoten ausgewählt:", nodeData);
}
});
// Formularfelder mit Mindmap verbinden
const nameInput = document.getElementById('name');
const descriptionInput = document.getElementById('description');
// Aktualisiere Mindmap wenn sich die Eingaben ändern
nameInput.addEventListener('input', function() {
if (mindmap.cy) {
const rootNode = mindmap.cy.$('#root');
if (rootNode.length > 0) {
rootNode.data('name', this.value || 'Neue Mindmap');
}
}
});
// Initialisiere die Mindmap
mindmap.initialize().then(() => {
console.log("Mindmap-Vorschau initialisiert");
// Setze initiale Werte
if (nameInput.value) {
const rootNode = mindmap.cy.$('#root');
if (rootNode.length > 0) {
rootNode.data('name', nameInput.value);
}
}
}).catch(error => {
console.error("Fehler bei der Initialisierung der Mindmap:", error);
});
});
</script>
{% endblock %}

View File

@@ -1,525 +0,0 @@
{% extends "base.html" %}
{% block title %}Mindmap bearbeiten{% endblock %}
{% block extra_css %}
<style>
/* Spezifische Stile für die Mindmap-Bearbeitungsseite */
.form-container {
background-color: var(--bg-secondary);
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
body.dark .form-container {
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.05);
}
body:not(.dark) .form-container {
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(0, 0, 0, 0.05);
}
.form-header {
padding: 1.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
body:not(.dark) .form-header {
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
.form-body {
padding: 1.5rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
}
.form-input,
.form-textarea {
width: 100%;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
transition: all 0.3s ease;
}
body.dark .form-input,
body.dark .form-textarea {
background-color: rgba(15, 23, 42, 0.6);
border: 1px solid rgba(255, 255, 255, 0.1);
color: #f1f5f9;
}
body:not(.dark) .form-input,
body:not(.dark) .form-textarea {
background-color: white;
border: 1px solid #e2e8f0;
color: #334155;
}
body.dark .form-input:focus,
body.dark .form-textarea:focus {
border-color: #7c3aed;
box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.2);
outline: none;
}
body:not(.dark) .form-input:focus,
body:not(.dark) .form-textarea:focus {
border-color: #7c3aed;
box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.2);
outline: none;
}
.form-textarea {
min-height: 120px;
resize: vertical;
}
.form-switch {
display: flex;
align-items: center;
}
.form-switch input[type="checkbox"] {
height: 0;
width: 0;
visibility: hidden;
position: absolute;
}
.form-switch label {
cursor: pointer;
width: 50px;
height: 25px;
background: rgba(100, 116, 139, 0.3);
display: block;
border-radius: 25px;
position: relative;
margin-right: 10px;
transition: all 0.3s ease;
}
.form-switch label:after {
content: '';
position: absolute;
top: 3px;
left: 3px;
width: 19px;
height: 19px;
background: #fff;
border-radius: 19px;
transition: 0.3s;
}
.form-switch input:checked + label {
background: #7c3aed;
}
.form-switch input:checked + label:after {
left: calc(100% - 3px);
transform: translateX(-100%);
}
.btn-submit {
background-color: #7c3aed;
color: white;
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
font-weight: 500;
transition: all 0.3s ease;
border: none;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.btn-submit:hover {
background-color: #6d28d9;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(109, 40, 217, 0.2);
}
.btn-cancel {
background-color: transparent;
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
font-weight: 500;
transition: all 0.3s ease;
border: 1px solid;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
body.dark .btn-cancel {
color: #e2e8f0;
border-color: rgba(255, 255, 255, 0.1);
}
body:not(.dark) .btn-cancel {
color: #475569;
border-color: #e2e8f0;
}
.btn-cancel:hover {
transform: translateY(-2px);
}
body.dark .btn-cancel:hover {
background-color: rgba(255, 255, 255, 0.05);
}
body:not(.dark) .btn-cancel:hover {
background-color: rgba(0, 0, 0, 0.05);
}
/* Animation für den Seiteneintritt */
@keyframes slideInUp {
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.form-container {
animation: slideInUp 0.5s ease forwards;
}
/* Animation für Hover-Effekte */
.input-animation {
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.input-animation:focus {
transform: scale(1.01);
}
</style>
{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8 animate-fadeIn">
<div class="max-w-3xl mx-auto">
<!-- Titel mit Animation -->
<div class="text-center mb-8 animate-pulse">
<h1 class="text-3xl font-bold mb-2 mystical-glow gradient-text">
Mindmap bearbeiten
</h1>
<p class="opacity-80">Aktualisiere die Details deiner Mindmap</p>
</div>
<div class="form-container">
<div class="form-header">
<h2 class="text-xl font-semibold">Mindmap-Details</h2>
</div>
<div class="form-body">
<form id="edit-mindmap-form">
<div class="form-group">
<label for="name" class="form-label">Name der Mindmap</label>
<input type="text" id="name" name="name" class="form-input input-animation" required
placeholder="z.B. Meine Philosophie-Mindmap" value="{{ mindmap.name }}">
</div>
<div class="form-group">
<label for="description" class="form-label">Beschreibung</label>
<textarea id="description" name="description" class="form-textarea input-animation"
placeholder="Worum geht es in dieser Mindmap?">{{ mindmap.description }}</textarea>
</div>
<div class="form-group">
<div class="form-switch">
<input type="checkbox" id="is_private" name="is_private" {% if mindmap.is_private %}checked{% endif %}>
<label for="is_private"></label>
<span>Private Mindmap (nur für dich sichtbar)</span>
</div>
</div>
<div class="flex justify-between mt-6">
<a href="{{ url_for('my_account') }}" class="btn-cancel"> {# Zurück zur Kontoübersicht geändert #}
<i class="fas fa-arrow-left"></i>
Zurück
</a>
<button type="button" id="save-mindmap-details-btn" class="btn-submit"> {# type="button" und ID hinzugefügt #}
<i class="fas fa-save"></i>
Änderungen speichern
</button>
</div>
</form>
<!-- Mindmap-Editor -->
<div class="mt-8">
<h3 class="text-xl font-semibold mb-4">Mindmap bearbeiten</h3>
<div class="mindmap-container">
<div id="cy" class="w-full h-[600px] rounded-xl border"
x-bind:class="darkMode ? 'border-gray-700' : 'border-gray-200'">
</div>
</div>
<!-- Bearbeitungshinweise -->
<div class="mt-4 text-sm opacity-80">
<p><i class="fas fa-info-circle mr-2"></i>Klicke auf Knoten zum Bearbeiten, ziehe sie zum Neuanordnen oder nutze die Toolbar für weitere Funktionen.</p>
</div>
</div>
</div>
</div>
<!-- Tipps-Sektion -->
<div class="mt-8 p-5 rounded-lg border animate-fadeIn"
x-bind:class="darkMode ? 'bg-slate-800/40 border-slate-700/50' : 'bg-white border-slate-200'">
<h3 class="text-xl font-semibold mb-3"
x-bind:class="darkMode ? 'text-white' : 'text-gray-800'">
<i class="fa-solid fa-lightbulb text-yellow-400 mr-2"></i>Tipps zum Bearbeiten einer Mindmap
</h3>
<div x-bind:class="darkMode ? 'text-gray-300' : 'text-gray-600'">
<ul class="list-disc pl-5 space-y-2">
<li>Überprüfe, ob der Name noch zum aktuellen Inhalt passt</li>
<li>Aktualisiere die Beschreibung, um neue Aspekte zu berücksichtigen</li>
<li>Entscheide, ob die Sichtbarkeitseinstellungen noch passend sind</li>
<li>Nutze aussagekräftige Namen für bessere Auffindbarkeit</li>
<li>Behalte die Konsistenz mit verknüpften Konzepten im Auge</li>
</ul>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.26.0/cytoscape.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape-cose-bilkent/4.1.0/cytoscape-cose-bilkent.min.js"></script>
<script nonce="{{ csp_nonce }}">
document.addEventListener('DOMContentLoaded', function() {
// Einfache Animationen für die Eingabefelder
const inputs = document.querySelectorAll('.input-animation');
inputs.forEach(input => {
// Subtile Skalierung bei Fokus
input.addEventListener('focus', function() {
this.style.transform = 'scale(1.01)';
this.style.boxShadow = '0 4px 12px rgba(124, 58, 237, 0.15)';
});
input.addEventListener('blur', function() {
this.style.transform = 'scale(1)';
this.style.boxShadow = 'none';
});
});
// Formular-Absenden-Logik für Metadaten
const editMindmapForm = document.getElementById('edit-mindmap-form');
const saveDetailsBtn = document.getElementById('save-mindmap-details-btn');
if (saveDetailsBtn && editMindmapForm) {
saveDetailsBtn.addEventListener('click', async function(event) {
event.preventDefault();
const nameInput = document.getElementById('name');
const descriptionInput = document.getElementById('description');
const isPrivateInput = document.getElementById('is_private');
const mindmapId = "{{ mindmap.id }}"; // Sicherstellen, dass mindmap.id hier verfügbar ist
const data = {
name: nameInput.value,
description: descriptionInput.value,
is_private: isPrivateInput.checked
// Die 'data' (Knoten/Kanten) wird separat vom Cytoscape-Editor gehandhabt
};
saveDetailsBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Wird gespeichert...';
saveDetailsBtn.disabled = true;
try {
const response = await fetch(`/api/mindmaps/${mindmapId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
if (response.ok) {
const result = await response.json();
showStatus('Metadaten erfolgreich gespeichert!', false);
// Optional: Weiterleitung oder Aktualisierung der Seiteninhalte
// window.location.href = "{{ url_for('my_account') }}";
} else {
const errorData = await response.json();
console.error('Fehler beim Speichern der Metadaten:', errorData);
showStatus(`Fehler: ${errorData.error || response.statusText}`, true);
}
} catch (error) {
console.error('Netzwerkfehler oder anderer Fehler:', error);
showStatus('Speichern fehlgeschlagen. Netzwerkproblem?', true);
} finally {
saveDetailsBtn.innerHTML = '<i class="fas fa-save"></i> Änderungen speichern';
saveDetailsBtn.disabled = false;
}
});
}
// Mindmap initialisieren
const mindmap = new MindMap.Visualization('cy', {
enableEditing: true,
apiEndpoint: '/api/mindmap/{{ mindmap.id }}',
onNodeClick: function(nodeData) {
console.log("Knoten ausgewählt:", nodeData);
},
onChange: function(dataFromCytoscape) {
// Automatisches Speichern bei Änderungen der Mindmap-Struktur
// Die Metadaten (Name, Beschreibung, is_private) werden separat über das Formular oben gespeichert.
// Diese onChange Funktion kümmert sich nur um die Strukturdaten (Knoten/Kanten).
const mindmapId = "{{ mindmap.id }}";
// Debounce-Funktion, um API-Aufrufe zu limitieren
let debounceTimer;
const debounceSaveStructure = (currentMindmapData) => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
// Der Backend-Endpunkt PUT /api/mindmaps/<id> erwartet ein Objekt,
// das die zu aktualisierenden Felder enthält. Für die Struktur ist das 'data'.
const payload = {
data: currentMindmapData // Dies sind die von Cytoscape gelieferten Strukturdaten
};
// showStatus('Speichere Struktur...', false); // Status wird jetzt über Event gehandhabt
fetch(`/api/mindmaps/${mindmapId}`, { // Endpunkt angepasst
method: 'PUT', // Methode zu PUT geändert
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload) // Sende die Mindmap-Daten als { data: ... }
}).then(response => {
if (!response.ok) {
response.json().then(err => {
console.error('Fehler beim Speichern der Struktur:', err);
document.dispatchEvent(new CustomEvent('mindmapError', { detail: { message: `Struktur: ${err.message || err.error || 'Speicherfehler'}` } }));
}).catch(() => {
console.error('Fehler beim Speichern der Struktur, Status:', response.statusText);
document.dispatchEvent(new CustomEvent('mindmapError', { detail: { message: `Struktur: ${response.statusText}` } }));
});
// throw new Error('Netzwerkfehler beim Speichern der Struktur'); // Wird schon behandelt
return; // Verhindere weitere Verarbeitung bei Fehler
}
return response.json();
}).then(responseData => {
if (responseData) { // Nur wenn response.ok war
console.log('Mindmap-Struktur erfolgreich gespeichert:', responseData);
// Die responseData von einem PUT könnte die aktualisierte Mindmap oder nur eine Erfolgsmeldung sein.
// Annahme: { message: "Mindmap updated successfully", mindmap: { ... } } oder ähnlich
document.dispatchEvent(new CustomEvent('mindmapSaved', { detail: { message: 'Struktur aktualisiert!' }}));
}
}).catch(error => {
console.error('Netzwerkfehler oder anderer Fehler beim Speichern der Struktur:', error);
// Vermeide doppelte Fehlermeldung, falls schon durch !response.ok behandelt
if (!document.querySelector('.bg-red-500')) { // Prüft, ob schon eine Fehlermeldung angezeigt wird
document.dispatchEvent(new CustomEvent('mindmapError', { detail: { message: 'Struktur: Netzwerkfehler' } }));
}
});
}, 1500); // Speichern 1.5 Sekunden nach der letzten Änderung
};
debounceSaveStructure(dataFromCytoscape); // Aufruf der Debounce-Funktion mit Cytoscape-Daten
}
});
// Die Verknüpfung der Formularfelder (Name, Beschreibung) mit dem Cytoscape Root-Knoten wird entfernt,
// da die Metadaten nun über das separate Formular oben gespeichert werden und nicht mehr direkt
// die Cytoscape-Daten manipulieren sollen. Die Logik für mindmap.saveToServer() wurde entfernt,
// da das Speichern jetzt über den onChange Handler mit PUT /api/mindmaps/<id> erfolgt.
// const nameInput = document.getElementById('name'); // Bereits oben deklariert für Metadaten
// nameInput.removeEventListener('input', ...); // Event Listener muss hier nicht entfernt werden, da er nicht neu hinzugefügt wird.
// Initialisiere die Mindmap mit existierenden Daten
mindmap.initialize().then(() => {
console.log("Mindmap-Editor initialisiert");
const mindmapId = "{{ mindmap.id }}";
// Lade existierende Daten für die Mindmap-Struktur
fetch(`/api/mindmaps/${mindmapId}`, { // Endpunkt für GET angepasst
method: 'GET',
headers: {
'Accept': 'application/json'
}
})
.then(response => {
if (!response.ok) {
response.json().then(err => {
showStatus(`Fehler beim Laden: ${err.message || err.error || response.statusText}`, true);
}).catch(() => {
showStatus(`Fehler beim Laden: ${response.statusText}`, true);
});
throw new Error(`Netzwerkantwort war nicht ok: ${response.statusText}`);
}
return response.json();
})
.then(mindmapDataFromServer => {
// Die API GET /api/mindmaps/<id> gibt ein Objekt zurück, das { id, name, description, is_private, data, ... } enthält.
// Wir brauchen nur den 'data'-Teil (Struktur) für Cytoscape.
// Die Metadaten (name, description, is_private) werden bereits serverseitig in die Formularfelder gerendert.
if (mindmapDataFromServer && mindmapDataFromServer.data) {
mindmap.loadData(mindmapDataFromServer.data); // Lade nur die Strukturdaten
console.log("Mindmap-Strukturdaten geladen:", mindmapDataFromServer.data);
showStatus("Mindmap geladen.", false);
} else {
console.error("Fehler: Mindmap-Daten (Struktur) nicht im erwarteten Format:", mindmapDataFromServer);
showStatus("Fehler: Mindmap-Struktur konnte nicht geladen werden (Formatfehler).", true);
}
})
.catch(error => {
console.error("Fehler beim Laden der Mindmap-Strukturdaten:", error);
if (!document.querySelector('.bg-red-500')) { // Prüft, ob schon eine Fehlermeldung angezeigt wird
showStatus("Laden der Struktur fehlgeschlagen.", true);
}
});
}).catch(error => {
console.error("Fehler bei der Initialisierung des Editors:", error);
});
// Autosave-Status Anzeige
const statusIndicator = document.createElement('div');
statusIndicator.className = 'fixed bottom-4 right-4 px-4 py-2 rounded-full text-sm transition-all duration-300';
document.body.appendChild(statusIndicator);
// Zeige Speicherstatus
function showStatus(message, isError = false) {
statusIndicator.textContent = message;
statusIndicator.className = `fixed bottom-4 right-4 px-4 py-2 rounded-full text-sm transition-all duration-300 ${
isError
? 'bg-red-500 text-white'
: 'bg-green-500 text-white'
}`;
setTimeout(() => {
statusIndicator.className = 'fixed bottom-4 right-4 px-4 py-2 rounded-full text-sm transition-all duration-300 opacity-0';
}, 2000);
}
// Event-Listener für Speicherstatus
document.addEventListener('mindmapSaved', (event) => {
const message = event.detail && event.detail.message ? event.detail.message : 'Erfolgreich gespeichert!';
showStatus(message, false);
});
document.addEventListener('mindmapError', (event) => {
showStatus(event.detail.message, true);
});
});
</script>
{% endblock %}

View File

@@ -1,48 +0,0 @@
{% extends "base.html" %}
{% block title %}400 - Ungültige Anfrage{% endblock %}
{% block content %}
<div class="container mx-auto max-w-4xl px-4 py-8">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl p-8 border border-gray-200 dark:border-gray-700">
<div class="text-center mb-8">
<div class="text-6xl font-bold text-red-500 mb-4">400</div>
<h1 class="text-3xl font-bold mb-2">Ungültige Anfrage</h1>
<p class="text-gray-600 dark:text-gray-400">Die Anfrage konnte nicht verarbeitet werden.</p>
</div>
<div class="mb-8 p-4 border border-red-200 dark:border-red-900 bg-red-50 dark:bg-red-900/20 rounded-lg">
<div class="flex items-start">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-red-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-800 dark:text-red-400">Fehlerbeschreibung</h3>
<div class="mt-2 text-sm text-red-700 dark:text-red-300">
{% if error %}
<p>{{ error }}</p>
{% else %}
<p>Die Anfrage enthält ungültige oder fehlerhafte Daten und konnte nicht verarbeitet werden.</p>
{% endif %}
</div>
</div>
</div>
</div>
<div class="text-center">
<p class="mb-4 text-gray-600 dark:text-gray-400">Hier sind einige Dinge, die Sie versuchen können:</p>
<ul class="list-disc list-inside text-left max-w-md mx-auto mb-6 text-gray-600 dark:text-gray-400">
<li>Überprüfen Sie Ihre Eingaben auf Fehler.</li>
<li>Stellen Sie sicher, dass Sie die richtigen Daten übermittelt haben.</li>
<li>Versuchen Sie, die Seite neu zu laden.</li>
<li>Kehren Sie zur Startseite zurück und versuchen Sie es erneut.</li>
</ul>
<a href="{{ url_for('index') }}" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Zurück zur Startseite
</a>
</div>
</div>
</div>
{% endblock %}

Some files were not shown because too many files have changed in this diff Show More