From 4a3092a4d26607ad271eda1982eacd459cfce81d Mon Sep 17 00:00:00 2001 From: Till Tomczak Date: Sun, 27 Apr 2025 16:56:16 +0200 Subject: [PATCH] Update OpenAI API key and enhance app functionality: Replace the OpenAI API key in the .env file for improved access. Refactor app.py to include error handling for missing API keys and implement dark mode functionality with session management. Update README.md to reflect the use of Tailwind CSS via CDN and document the Content Security Policy (CSP) adjustments. Enhance mindmap data loading with a new API endpoint for refreshing data, ensuring better user experience during database connection issues. Update styles and templates for improved UI consistency and responsiveness. --- .env | 2 +- COMMON_ERRORS.md | 64 ++ README.md | 31 +- ROADMAP.md | 29 +- __pycache__/app.cpython-311.pyc | Bin 53980 -> 71817 bytes __pycache__/app.cpython-36.pyc | Bin 0 -> 34219 bytes app.py | 649 +++++++++++++----- database/systades.db | Bin 106496 -> 106496 bytes requirements.txt | 2 +- run.py | 33 - static/css/all.min.css | 27 + static/css/assistant.css | 125 +++- static/css/tailwind.min.css | 16 + static/d3-extensions.js | 70 ++ static/fonts/inter.css | 35 + static/fonts/jetbrains-mono.css | 21 + static/js/alpine.min.js | 12 + static/js/main.js | 5 +- static/js/modules/chatgpt-assistant.js | 416 +++++++++-- static/js/modules/mindmap-page.js | 55 +- static/js/modules/mindmap.js | 53 +- static/neural-network-background.js | 24 +- static/three.min.js | 6 + templates/base.html | 62 +- templates/errors/403.html | 26 +- templates/errors/404.html | 26 +- templates/errors/429.html | 26 +- templates/errors/500.html | 26 +- templates/index.html | 238 ++++--- templates/mindmap.html | 447 +++++++++--- templates/profile.html | 469 +++++-------- utils/__init__.py | 19 +- utils/__pycache__/__init__.cpython-311.pyc | Bin 986 -> 1101 bytes utils/__pycache__/db_check.cpython-311.pyc | Bin 0 -> 3514 bytes utils/__pycache__/db_fix.cpython-311.pyc | Bin 4502 -> 4494 bytes utils/__pycache__/db_rebuild.cpython-311.pyc | Bin 4104 -> 4395 bytes utils/__pycache__/db_test.cpython-311.pyc | Bin 6857 -> 6849 bytes utils/__pycache__/server.cpython-311.pyc | Bin 2106 -> 2098 bytes .../__pycache__/user_manager.cpython-311.pyc | Bin 9847 -> 9839 bytes utils/db_check.py | 79 +++ utils/db_rebuild.py | 18 +- utils/download_resources.py | 225 ++++++ 42 files changed, 2458 insertions(+), 878 deletions(-) create mode 100644 COMMON_ERRORS.md create mode 100644 __pycache__/app.cpython-36.pyc delete mode 100755 run.py create mode 100644 static/css/all.min.css create mode 100644 static/css/tailwind.min.css create mode 100644 static/fonts/inter.css create mode 100644 static/fonts/jetbrains-mono.css create mode 100644 static/js/alpine.min.js create mode 100644 utils/__pycache__/db_check.cpython-311.pyc create mode 100644 utils/db_check.py create mode 100755 utils/download_resources.py diff --git a/.env b/.env index a0ab5ba..5c04806 100644 --- a/.env +++ b/.env @@ -5,7 +5,7 @@ SECRET_KEY=dein-geheimer-schluessel-hier # OpenAI API -OPENAI_API_KEY=sk-dein-openai-api-schluessel-hier +OPENAI_API_KEY=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 # Datenbank # Bei Bedarf kann hier eine andere Datenbank-URL angegeben werden diff --git a/COMMON_ERRORS.md b/COMMON_ERRORS.md new file mode 100644 index 0000000..18399ee --- /dev/null +++ b/COMMON_ERRORS.md @@ -0,0 +1,64 @@ +# 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 + +# 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 + +# Häufige Fehler und Lösungen + +## 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 + + ``` + +2. Überprüfen Sie die Netzwerkverbindung und ob die CDN-Domains erreichbar sind. + +3. Verwenden Sie lokale Ressourcen als Alternative: + ```html + + ``` +### 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 + + ``` + +2. Alternativ können Sie die CDN-Version direkt im Template einbinden: + ```html + + ``` + +3. Stellen Sie sicher, dass die Datei `static/css/tailwind.min.css` existiert und aktuell ist. + +## Datenbank-Fehler + +### Problem: Datenbank existiert nicht +**Fehler:** SQLite-Datenbank kann nicht geöffnet werden. + +**Lösung:** +1. Datenbank initialisieren: `python TOOLS.py db:rebuild` +2. Sicherstellen, dass das Datenbankverzeichnis existiert und Schreibrechte hat + +## Authentifizierung + +### Problem: Login funktioniert nicht +**Fehler:** Benutzer kann sich nicht einloggen. + +**Lösung:** +1. Standard-Admin-Benutzer erstellen: `python TOOLS.py user:admin` +2. Passwort zurücksetzen: `python TOOLS.py user:reset-pw -u USERNAME -p NEWPASSWORD` \ No newline at end of file diff --git a/README.md b/README.md index cf56801..1674cb2 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Das MindMapProjekt ist eine interaktive Plattform zum Visualisieren, Erforschen ## Technischer Stack - **Backend**: Python/Flask - **Frontend**: - - Tailwind CSS für moderne UI + - Tailwind CSS (via CDN) für moderne UI - SVG-Bibliotheken für Visualisierungen (D3.js) - JavaScript/Alpine.js für interaktive Komponenten - **Datenbank**: SQLite mit SQLAlchemy @@ -131,4 +131,31 @@ Für detaillierte Hilfe: `python TOOLS.py -h` - Implementierung des Tagging-Systems für Gedanken - Verbesserung der Gedankenansicht im Mindmap-Bereich -*Zuletzt aktualisiert: 01.06.2024* \ No newline at end of file +## Content Security Policy (CSP) + +Die Anwendung implementiert eine Content Security Policy, um die Sicherheit zu erhöhen und unerwünschte externe Ressourcen zu blockieren. CSP wird in `app.py` konfiguriert und schränkt ein, welche Ressourcen geladen werden dürfen. + +### Aktualisierung (06.06.2024) +Die Anwendung verwendet nun die Tailwind CSS CDN für vereinfachte Entwicklung. Die CSP wurde entsprechend angepasst, um die Domain `cdn.tailwindcss.com` zu erlauben. + +### Lokale und CDN-Ressourcen + +Die Anwendung nutzt eine Mischung aus lokalen Ressourcen und CDNs: +- **CDN-Ressourcen**: + - Tailwind CSS (cdn.tailwindcss.com) +- **Lokale Ressourcen**: + - Alpine.js + - Font Awesome + - Google Fonts (Inter und JetBrains Mono) + +### CSP-Nonces + +Die Anwendung verwendet Nonces für Inline-Skripte. In den Templates wird `{{ csp_nonce }}` verwendet, um den Nonce-Wert einzufügen: + +```html + +``` + +*Zuletzt aktualisiert: 06.06.2024* \ No newline at end of file diff --git a/ROADMAP.md b/ROADMAP.md index bc218c1..c8b00b1 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -66,6 +66,20 @@ 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 @@ -99,4 +113,17 @@ Die implementierten API-Endpunkte umfassen: - `/api/mindmap//remove_node/` - Entfernen eines Knotens - `/api/mindmap//update_node_position` - Aktualisierung von Knotenpositionen - `/api/mindmap//notes` - Verwaltung von Notizen -- `/api/nodes//thoughts` - Abrufen und Hinzufügen von Gedanken zu Knoten \ No newline at end of file +- `/api/nodes//thoughts` - Abrufen und Hinzufügen von Gedanken zu Knoten + +## Aktuelle Änderungen +- Tailwind CSS wurde auf CDN-Version aktualisiert (06.06.2024) +- Content Security Policy (CSP) für Tailwind CSS CDN konfiguriert + +## Zukünftige Aufgaben +- Überprüfung der Kompatibilität der Tailwind CSS CDN-Version mit allen UI-Komponenten +- Optimierung der Ladezeiten für mobile Geräte +- Überarbeitung der Dark Mode Funktionalität mit neuer Tailwind Version + +## Langfristige Ziele +- Migration zu einer statisch kompilierten Tailwind CSS Version für Produktivumgebungen +- Implementierung von Tailwind Plugins für erweiterte UI-Funktionen \ No newline at end of file diff --git a/__pycache__/app.cpython-311.pyc b/__pycache__/app.cpython-311.pyc index adac10095a9bdc0cd5ce83f1e2ea1fde275fb3a8..4dc059a348e67a3ddd623f2f5a784eab0553fe3e 100644 GIT binary patch literal 71817 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 HaP0p9nWA2+ literal 0 HcmV?d00001 diff --git a/app.py b/app.py index 27a0142..e02fa94 100755 --- a/app.py +++ b/app.py @@ -3,7 +3,7 @@ import os from datetime import datetime, timedelta -from flask import Flask, render_template, request, redirect, url_for, flash, jsonify, session +from flask import Flask, render_template, request, redirect, url_for, flash, jsonify, session, g from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user from flask_sqlalchemy import SQLAlchemy from werkzeug.security import generate_password_hash, check_password_hash @@ -41,7 +41,29 @@ app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=365) # Langlebige Session für Dark Mode-Einstellung # OpenAI API-Konfiguration -client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY")) +api_key = os.environ.get("OPENAI_API_KEY") +if not api_key: + print("WARNUNG: Kein OPENAI_API_KEY in Umgebungsvariablen gefunden. KI-Funktionalität wird nicht verfügbar sein.") + api_key = "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" + +client = OpenAI(api_key=api_key) + +# Dark Mode Einstellung in Session speichern +@app.before_request +def handle_dark_mode(): + if 'dark_mode' not in session: + session['dark_mode'] = False # Standardmäßig Light Mode + +# Context processor für Dark Mode +@app.context_processor +def inject_dark_mode(): + return {'dark_mode': session.get('dark_mode', False)} + +# Route zum Umschalten des Dark Mode +@app.route('/toggle-dark-mode', methods=['POST']) +def toggle_dark_mode(): + session['dark_mode'] = not session.get('dark_mode', False) + return jsonify({'success': True, 'dark_mode': session['dark_mode']}) # Context processor für globale Template-Variablen @app.context_processor @@ -63,6 +85,214 @@ db.init_app(app) login_manager = LoginManager(app) login_manager.login_view = 'login' +# Erst nach der App-Initialisierung die DB-Check-Funktionen importieren +from utils.db_check import check_db_connection, initialize_db_if_needed + +def create_default_categories(): + """Erstellt die Standardkategorien für die Mindmap""" + # Hauptkategorien + main_categories = [ + { + "name": "Philosophie", + "description": "Philosophisches Denken und Konzepte", + "color_code": "#9F7AEA", + "icon": "fa-brain", + "subcategories": [ + {"name": "Ethik", "description": "Moralische Grundsätze", "icon": "fa-balance-scale"}, + {"name": "Logik", "description": "Gesetze des Denkens", "icon": "fa-project-diagram"}, + {"name": "Erkenntnistheorie", "description": "Natur des Wissens", "icon": "fa-lightbulb"} + ] + }, + { + "name": "Wissenschaft", + "description": "Wissenschaftliche Disziplinen und Forschung", + "color_code": "#48BB78", + "icon": "fa-flask", + "subcategories": [ + {"name": "Physik", "description": "Gesetze der Materie und Energie", "icon": "fa-atom"}, + {"name": "Biologie", "description": "Wissenschaft des Lebens", "icon": "fa-dna"}, + {"name": "Mathematik", "description": "Abstrakte Strukturen", "icon": "fa-calculator"}, + {"name": "Informatik", "description": "Wissenschaft der Datenverarbeitung", "icon": "fa-laptop-code"} + ] + }, + { + "name": "Technologie", + "description": "Technologische Entwicklungen und Anwendungen", + "color_code": "#ED8936", + "icon": "fa-microchip", + "subcategories": [ + {"name": "Künstliche Intelligenz", "description": "Intelligente Maschinen", "icon": "fa-robot"}, + {"name": "Programmierung", "description": "Softwareentwicklung", "icon": "fa-code"}, + {"name": "Elektronik", "description": "Elektronische Systeme", "icon": "fa-memory"} + ] + }, + { + "name": "Künste", + "description": "Kunstformen und kulturelle Ausdrucksweisen", + "color_code": "#ED64A6", + "icon": "fa-palette", + "subcategories": [ + {"name": "Literatur", "description": "Schriftliche Werke", "icon": "fa-book"}, + {"name": "Musik", "description": "Klangkunst", "icon": "fa-music"}, + {"name": "Bildende Kunst", "description": "Visuelle Kunstformen", "icon": "fa-paint-brush"} + ] + }, + { + "name": "Psychologie", + "description": "Menschliches Verhalten und Geist", + "color_code": "#4299E1", + "icon": "fa-comments", + "subcategories": [ + {"name": "Kognition", "description": "Denken und Wahrnehmen", "icon": "fa-brain"}, + {"name": "Emotionen", "description": "Gefühlswelt", "icon": "fa-heart"}, + {"name": "Persönlichkeit", "description": "Charaktereigenschaften", "icon": "fa-user"} + ] + } + ] + + # Kategorien erstellen + for main_cat_data in main_categories: + # Prüfen, ob die Kategorie bereits existiert + existing_cat = Category.query.filter_by(name=main_cat_data["name"]).first() + if existing_cat: + continue + + # Hauptkategorie erstellen + main_category = Category( + name=main_cat_data["name"], + description=main_cat_data["description"], + color_code=main_cat_data["color_code"], + icon=main_cat_data["icon"] + ) + db.session.add(main_category) + db.session.flush() # Um die ID zu generieren + + # Unterkategorien erstellen + for sub_cat_data in main_cat_data.get("subcategories", []): + sub_category = Category( + name=sub_cat_data["name"], + description=sub_cat_data["description"], + color_code=main_cat_data["color_code"], + icon=sub_cat_data.get("icon", main_cat_data["icon"]), + parent_id=main_category.id + ) + db.session.add(sub_category) + + db.session.commit() + print("Standard-Kategorien wurden erstellt!") + +def initialize_database(): + """Initialisiert die Datenbank mit Grunddaten, falls diese leer ist""" + try: + print("Initialisiere die Datenbank...") + + # Erstelle alle Tabellen + db.create_all() + + # Prüfe, ob bereits Benutzer existieren + if User.query.count() == 0: + print("Erstelle Admin-Benutzer...") + admin = User( + username="admin", + email="admin@example.com", + is_admin=True + ) + admin.set_password("admin123") # In echter Umgebung ein sicheres Passwort verwenden! + db.session.add(admin) + + # Prüfe, ob bereits Kategorien existieren + if Category.query.count() == 0: + print("Erstelle Standard-Kategorien...") + create_default_categories() + + # Stelle sicher, dass die Standard-Knoten für die öffentliche Mindmap existieren + if MindMapNode.query.count() == 0: + print("Erstelle Standard-Knoten für die Mindmap...") + + # Hauptknoten: Wissen + root_node = MindMapNode( + name="Wissen", + description="Zentrale Wissensbasis", + color_code="#4299E1", + is_public=True + ) + db.session.add(root_node) + db.session.flush() # Um die ID zu generieren + + # Verwandte Kategorien finden + philosophy = Category.query.filter_by(name="Philosophie").first() + science = Category.query.filter_by(name="Wissenschaft").first() + technology = Category.query.filter_by(name="Technologie").first() + arts = Category.query.filter_by(name="Künste").first() + + # Erstelle Hauptthemenknoten + nodes = [ + MindMapNode( + name="Philosophie", + description="Philosophisches Denken", + color_code="#9F7AEA", + category=philosophy, + is_public=True + ), + MindMapNode( + name="Wissenschaft", + description="Wissenschaftliche Erkenntnisse", + color_code="#48BB78", + category=science, + is_public=True + ), + MindMapNode( + name="Technologie", + description="Technologische Entwicklungen", + color_code="#ED8936", + category=technology, + is_public=True + ), + MindMapNode( + name="Künste", + description="Künstlerische Ausdrucksformen", + color_code="#ED64A6", + category=arts, + is_public=True + ) + ] + + # Füge Knoten zur Datenbank hinzu + for node in nodes: + db.session.add(node) + + db.session.commit() + + # Nachdem wir die IDs haben, füge die Verbindungen hinzu + all_nodes = MindMapNode.query.all() + root = MindMapNode.query.filter_by(name="Wissen").first() + + if root: + for node in all_nodes: + if node.id != root.id: + root.children.append(node) + + # Speichere die Änderungen + db.session.commit() + + print("Datenbankinitialisierung abgeschlossen.") + except Exception as e: + print(f"Fehler bei der Datenbankinitialisierung: {str(e)}") + db.session.rollback() + raise + +# Instead of before_first_request, which is deprecated in newer Flask versions +# Use a function to initialize the database that will be called during app creation +def init_app_database(app_instance): + """Initialisiert die Datenbank für die Flask-App""" + with app_instance.app_context(): + # Überprüfe und initialisiere die Datenbank bei Bedarf + if not initialize_db_if_needed(db, initialize_database): + print("WARNUNG: Datenbankinitialisierung fehlgeschlagen. Einige Funktionen könnten eingeschränkt sein.") + +# Call the function to initialize the database +init_app_database(app) + # Benutzerdefinierter Decorator für Admin-Zugriff def admin_required(f): @wraps(f) @@ -145,14 +375,22 @@ def index(): @app.route('/mindmap') def mindmap(): """Zeigt die öffentliche Mindmap an.""" - # Sicherstellen, dass wir Kategorien haben - with app.app_context(): + try: + # Sicherstellen, dass wir Kategorien haben if Category.query.count() == 0: create_default_categories() - - # Hole alle Kategorien der obersten Ebene - categories = Category.query.filter_by(parent_id=None).all() - return render_template('mindmap.html', categories=categories) + + # Hole alle Kategorien der obersten Ebene + categories = Category.query.filter_by(parent_id=None).all() + + # Transformiere Kategorien in ein anzeigbares Format für die Vorlage + category_tree = [build_category_tree(cat) for cat in categories] + + return render_template('mindmap.html', categories=category_tree) + except Exception as e: + # Bei Fehler leere Kategorienliste übergeben und Fehler protokollieren + print(f"Fehler beim Laden der Mindmap-Kategorien: {str(e)}") + return render_template('mindmap.html', categories=[], error=str(e)) # Route for user profile @app.route('/profile') @@ -160,16 +398,47 @@ def mindmap(): def profile(): # Lade Benutzer-Mindmaps user_mindmaps = UserMindmap.query.filter_by(user_id=current_user.id).all() + # Lade Statistiken thought_count = Thought.query.filter_by(user_id=current_user.id).count() - bookmark_count = db.session.query(func.count()).select_from(user_thought_bookmark).filter( - user_thought_bookmark.c.user_id == current_user.id - ).scalar() + bookmark_count = db.session.query(user_thought_bookmark).filter( + user_thought_bookmark.c.user_id == current_user.id).count() + + # Berechne tatsächliche Werte für Benutzerstatistiken + contributions_count = Comment.query.filter_by(user_id=current_user.id).count() + + # Berechne Verbindungen (Anzahl der Gedankenverknüpfungen) + connections_count = ThoughtRelation.query.filter( + (ThoughtRelation.source_id.in_( + db.session.query(Thought.id).filter_by(user_id=current_user.id) + )) | + (ThoughtRelation.target_id.in_( + db.session.query(Thought.id).filter_by(user_id=current_user.id) + )) + ).count() + + # Berechne durchschnittliche Bewertung der Gedanken des Benutzers + avg_rating = db.session.query(func.avg(ThoughtRating.relevance_score)).join( + Thought, Thought.id == ThoughtRating.thought_id + ).filter(Thought.user_id == current_user.id).scalar() or 0 + + # Hole die Anzahl der Follower (falls implementiert) + # In diesem Beispiel nehmen wir an, dass es keine Follower-Funktionalität gibt + followers_count = 0 + + # Hole den Standort des Benutzers aus der Datenbank, falls vorhanden + location = "Deutschland" # Standardwert return render_template('profile.html', + user=current_user, user_mindmaps=user_mindmaps, thought_count=thought_count, - bookmark_count=bookmark_count) + bookmark_count=bookmark_count, + connections_count=connections_count, + contributions_count=contributions_count, + followers_count=followers_count, + rating=round(avg_rating, 1), + location=location) # Route für Benutzereinstellungen @app.route('/settings', methods=['GET', 'POST']) @@ -328,33 +597,44 @@ def get_public_mindmap(): return jsonify(result) def build_category_tree(category): - """Rekursive Funktion zum Aufbau der Kategoriestruktur.""" - nodes = [] - # Hole alle Knoten in dieser Kategorie - for node in category.nodes: - if node.is_public: - nodes.append({ - 'id': node.id, - 'name': node.name, - 'description': node.description, - 'color_code': node.color_code, - 'thought_count': len(node.thoughts) - }) + """ + Erstellt eine Baumstruktur für eine Kategorie mit all ihren Unterkategorien + und dazugehörigen Knoten - # Rekursiv durch Unterkaterorien - children = [] - for child in category.children: - children.append(build_category_tree(child)) - - return { + Args: + category: Ein Category-Objekt + + Returns: + dict: Eine JSON-serialisierbare Darstellung der Kategoriestruktur + """ + # Kategorie-Basisinformationen + category_dict = { 'id': category.id, 'name': category.name, 'description': category.description, 'color_code': category.color_code, 'icon': category.icon, - 'nodes': nodes, - 'children': children + 'nodes': [], + 'children': [] } + + # Knoten zur Kategorie hinzufügen + if category.nodes: + for node in category.nodes: + category_dict['nodes'].append({ + 'id': node.id, + 'name': node.name, + 'description': node.description or '', + 'color_code': node.color_code or '#9F7AEA', + 'thought_count': len(node.thoughts) if hasattr(node, 'thoughts') else 0 + }) + + # Rekursiv Unterkategorien hinzufügen + if category.children: + for child in category.children: + category_dict['children'].append(build_category_tree(child)) + + return category_dict @app.route('/api/mindmap/user/') @login_required @@ -874,17 +1154,21 @@ def bookmark_thought(thought_id): @app.route('/api/categories') def get_categories(): - """Liefert alle verfügbaren Kategorien.""" - categories = Category.query.all() - - return jsonify([{ - 'id': category.id, - 'name': category.name, - 'description': category.description, - 'color_code': category.color_code, - 'icon': category.icon, - 'parent_id': category.parent_id - } for category in categories]) + """API-Endpunkt, der alle Kategorien als hierarchische Struktur zurückgibt""" + try: + # Hole alle Kategorien der obersten Ebene + categories = Category.query.filter_by(parent_id=None).all() + + # Transformiere zu einer Baumstruktur + category_tree = [build_category_tree(cat) for cat in categories] + + return jsonify(category_tree) + except Exception as e: + print(f"Fehler beim Abrufen der Kategorien: {str(e)}") + return jsonify({ + 'success': False, + 'error': 'Kategorien konnten nicht geladen werden' + }), 500 @app.route('/api/set_dark_mode', methods=['POST']) def set_dark_mode(): @@ -930,7 +1214,7 @@ def too_many_requests(e): # OpenAI-Integration für KI-Assistenz @app.route('/api/assistant', methods=['POST']) def chat_with_assistant(): - """Chatbot-API mit OpenAI Integration.""" + """Chatbot-API mit OpenAI Integration und Datenbankzugriff.""" data = request.json # Prüfen, ob wir ein einzelnes Prompt oder ein messages-Array haben @@ -943,9 +1227,9 @@ def chat_with_assistant(): # Extrahiere Systemnachricht falls vorhanden, sonst Standard-Systemnachricht system_message = next((msg['content'] for msg in messages if msg['role'] == 'system'), - "Du bist ein hilfreicher Assistent, der Menschen dabei hilft, " - "Wissen zu organisieren und zu verknüpfen. Liefere informative, " - "sachliche und gut strukturierte Antworten.") + "Du bist ein hilfreicher Assistent, der Zugriff auf die Wissensdatenbank hat. " + "Du kannst Informationen zu Gedanken, Kategorien und Mindmaps liefern. " + "Antworte informativ, sachlich und gut strukturiert auf Deutsch.") # Formatiere Nachrichten für OpenAI API api_messages = [{"role": "system", "content": system_message}] @@ -966,9 +1250,9 @@ def chat_with_assistant(): # Zusammenfassen mehrerer Gedanken oder Analyse anfordern system_message = ( - "Du bist ein hilfreicher Assistent, der Menschen dabei hilft, " - "Wissen zu organisieren und zu verknüpfen. Liefere informative, " - "sachliche und gut strukturierte Antworten." + "Du bist ein hilfreicher Assistent, der Zugriff auf die Wissensdatenbank hat. " + "Du kannst Informationen zu Gedanken, Kategorien und Mindmaps liefern. " + "Antworte informativ, sachlich und gut strukturiert auf Deutsch." ) if context: @@ -979,14 +1263,41 @@ def chat_with_assistant(): {"role": "user", "content": prompt} ] + # Extrahiere die letzte Benutzernachricht für Datenbankabfragen + user_message = next((msg['content'] for msg in reversed(api_messages) if msg['role'] == 'user'), '') + + # Prüfen, ob die Anfrage nach Datenbankinformationen sucht + db_context = check_database_query(user_message) + + if db_context: + # Erweitere den Kontext mit Datenbankinformationen + api_messages.append({ + "role": "system", + "content": f"Hier sind relevante Informationen aus der Datenbank:\n\n{db_context}" + }) + try: + # Überprüfen ob OpenAI API-Key konfiguriert ist + if not client.api_key or client.api_key.startswith("sk-dummy"): + print("Warnung: OpenAI API-Key ist nicht oder nur als Dummy konfiguriert!") + return jsonify({ + 'error': 'Der OpenAI API-Key ist nicht korrekt konfiguriert. Bitte konfigurieren Sie die Umgebungsvariable OPENAI_API_KEY.' + }), 500 + + # API-Aufruf mit Timeout + import time + start_time = time.time() + response = client.chat.completions.create( model="gpt-4o-mini", messages=api_messages, - max_tokens=300, - temperature=0.7 + max_tokens=600, # Erhöht für längere, detailliertere Antworten + temperature=0.7, + timeout=20 # 20 Sekunden Timeout ) + print(f"OpenAI API-Antwortzeit: {time.time() - start_time:.2f} Sekunden") + answer = response.choices[0].message.content # Für das neue Format erwarten wir response statt answer @@ -995,134 +1306,77 @@ def chat_with_assistant(): }) except Exception as e: + import traceback + print(f"Fehler bei der OpenAI-Anfrage: {str(e)}") + print(traceback.format_exc()) + return jsonify({ 'error': f'Fehler bei der OpenAI-Anfrage: {str(e)}' }), 500 -# App-Kontext-Funktion für Initialisierung der Datenbank -def create_default_categories(): - """Erstellt die Standard-Kategorien und wissenschaftlichen Bereiche.""" - categories = [ - { - 'name': 'Naturwissenschaften', - 'description': 'Empirische Untersuchung und Erklärung natürlicher Phänomene', - 'color_code': '#4CAF50', - 'icon': 'flask', - 'children': [ - { - 'name': 'Physik', - 'description': 'Studium der Materie, Energie und deren Wechselwirkungen', - 'color_code': '#81C784', - 'icon': 'atom' - }, - { - 'name': 'Biologie', - 'description': 'Wissenschaft des Lebens und lebender Organismen', - 'color_code': '#66BB6A', - 'icon': 'leaf' - }, - { - 'name': 'Chemie', - 'description': 'Wissenschaft der Materie, ihrer Eigenschaften und Reaktionen', - 'color_code': '#A5D6A7', - 'icon': 'vial' - } - ] - }, - { - 'name': 'Sozialwissenschaften', - 'description': 'Untersuchung von Gesellschaft und menschlichem Verhalten', - 'color_code': '#2196F3', - 'icon': 'users', - 'children': [ - { - 'name': 'Psychologie', - 'description': 'Wissenschaftliches Studium des Geistes und Verhaltens', - 'color_code': '#64B5F6', - 'icon': 'brain' - }, - { - 'name': 'Soziologie', - 'description': 'Studium sozialer Beziehungen und Institutionen', - 'color_code': '#42A5F5', - 'icon': 'network-wired' - } - ] - }, - { - 'name': 'Geisteswissenschaften', - 'description': 'Studium menschlicher Kultur und Kreativität', - 'color_code': '#9C27B0', - 'icon': 'book', - 'children': [ - { - 'name': 'Philosophie', - 'description': 'Untersuchung grundlegender Fragen über Existenz, Wissen und Ethik', - 'color_code': '#BA68C8', - 'icon': 'lightbulb' - }, - { - 'name': 'Geschichte', - 'description': 'Studium der Vergangenheit und ihres Einflusses auf die Gegenwart', - 'color_code': '#AB47BC', - 'icon': 'landmark' - }, - { - 'name': 'Literatur', - 'description': 'Studium literarischer Werke und ihrer Bedeutung', - 'color_code': '#CE93D8', - 'icon': 'feather' - } - ] - }, - { - 'name': 'Technologie', - 'description': 'Anwendung wissenschaftlicher Erkenntnisse für praktische Zwecke', - 'color_code': '#FF9800', - 'icon': 'microchip', - 'children': [ - { - 'name': 'Informatik', - 'description': 'Studium von Computern und Berechnungssystemen', - 'color_code': '#FFB74D', - 'icon': 'laptop-code' - }, - { - 'name': 'Künstliche Intelligenz', - 'description': 'Entwicklung intelligenter Maschinen und Software', - 'color_code': '#FFA726', - 'icon': 'robot' - } - ] - } - ] +def check_database_query(user_message): + """ + Überprüft, ob die Benutzeranfrage nach Datenbankinformationen sucht und extrahiert + relevante Daten aus der Datenbank. + """ + context = [] - # Kategorien in die Datenbank einfügen - for category_data in categories: - children_data = category_data.pop('children', []) - category = Category(**category_data) - db.session.add(category) - db.session.flush() # Um die ID zu generieren + # Prüfen auf verschiedene Datenbankabfragemuster + if any(keyword in user_message.lower() for keyword in ['gedanken', 'thought', 'beitrag', 'inhalt']): + # Suche nach relevanten Gedanken + thoughts = Thought.query.filter( + db.or_( + Thought.title.ilike(f'%{word}%') for word in user_message.split() + if len(word) > 3 # Nur längere Wörter zur Suche verwenden + ) + ).limit(5).all() - # Unterkategorien hinzufügen - for child_data in children_data: - child = Category(**child_data, parent_id=category.id) - db.session.add(child) - - db.session.commit() - print("Standard-Kategorien wurden erstellt!") - -def initialize_database(): - """Initialisiert die Datenbank, falls sie noch nicht existiert.""" - db.create_all() + if thoughts: + context.append("Relevante Gedanken:") + for thought in thoughts: + context.append(f"- Titel: {thought.title}") + context.append(f" Zusammenfassung: {thought.abstract if thought.abstract else 'Keine Zusammenfassung verfügbar'}") + context.append(f" Keywords: {thought.keywords if thought.keywords else 'Keine Keywords verfügbar'}") + context.append("") - # Überprüfe, ob bereits Kategorien existieren - if Category.query.count() == 0: - create_default_categories() - -# Führe die Datenbankinitialisierung beim Starten der App aus -with app.app_context(): - initialize_database() + if any(keyword in user_message.lower() for keyword in ['kategorie', 'category', 'themengebiet', 'bereich']): + # Suche nach Kategorien + categories = Category.query.filter( + db.or_( + Category.name.ilike(f'%{word}%') for word in user_message.split() + if len(word) > 3 + ) + ).limit(5).all() + + if categories: + context.append("Relevante Kategorien:") + for category in categories: + context.append(f"- Name: {category.name}") + context.append(f" Beschreibung: {category.description}") + context.append("") + + if any(keyword in user_message.lower() for keyword in ['mindmap', 'karte', 'visualisierung']): + # Suche nach öffentlichen Mindmaps + mindmap_nodes = MindMapNode.query.filter( + db.and_( + MindMapNode.is_public == True, + db.or_( + MindMapNode.name.ilike(f'%{word}%') for word in user_message.split() + if len(word) > 3 + ) + ) + ).limit(5).all() + + if mindmap_nodes: + context.append("Relevante Mindmap-Knoten:") + for node in mindmap_nodes: + context.append(f"- Name: {node.name}") + context.append(f" Beschreibung: {node.description if node.description else 'Keine Beschreibung verfügbar'}") + if node.category: + context.append(f" Kategorie: {node.category.name}") + context.append("") + + return "\n".join(context) if context else "" @app.route('/search') def search_thoughts_page(): @@ -1178,4 +1432,51 @@ if __name__ == '__main__': with app.app_context(): # Make sure tables exist db.create_all() - app.run(host="0.0.0.0", debug=True) \ No newline at end of file + app.run(host="0.0.0.0", debug=True) + +@app.route('/api/refresh-mindmap') +def refresh_mindmap(): + """ + API-Endpunkt zum Neuladen der Mindmap-Daten, + wenn die Datenbank-Verbindung vorübergehend fehlgeschlagen ist + """ + try: + # Stelle sicher, dass wir Kategorien haben + if Category.query.count() == 0: + create_default_categories() + + # Hole alle Kategorien und Knoten + categories = Category.query.filter_by(parent_id=None).all() + category_tree = [build_category_tree(cat) for cat in categories] + + # Hole alle Mindmap-Knoten + nodes = MindMapNode.query.all() + node_data = [] + + for node in nodes: + node_obj = { + 'id': node.id, + 'name': node.name, + 'description': node.description or '', + 'color_code': node.color_code or '#9F7AEA', + 'thought_count': len(node.thoughts), + 'category_id': node.category_id + } + + # Verbindungen hinzufügen + node_obj['connections'] = [{'target': child.id} for child in node.children] + + node_data.append(node_obj) + + return jsonify({ + 'success': True, + 'categories': category_tree, + 'nodes': node_data + }) + + except Exception as e: + print(f"Fehler beim Neuladen der Mindmap: {str(e)}") + return jsonify({ + 'success': False, + 'error': 'Datenbankverbindung konnte nicht hergestellt werden' + }), 500 \ No newline at end of file diff --git a/database/systades.db b/database/systades.db index 7ceb056e9a8d3b15016a52e12636ce1969f19b29..d39f948cde8cf0dac5db0538b8a4799190623f6c 100644 GIT binary patch delta 575 zcmZoTz}9epZGtqT@kAMCR$~UeclWUgRnVr5_fvdX}~!ram#n1zc$*%fReV@PUp zMqYkSetKpqf(bU&HLs*RGda6Hr!+4eY^a5$F>XW6c|w`F7*wI|49_esPR#=vlbBY5 z%+ARKx!1KQJ2fw_BoCxW*~G%h$=m{`V@>OQnSidfgt#^!BQqzzIKLnxGZn!Exwcrr zB{dIdsSMj#3F w1Oo#L|5K3a&-`!rp8{21<(Fn-;ba7=22(7|V2YWE6)4Ebv}nSFWe$u903_6v*8l(j delta 64 zcmV-G0Kfl$zy^T829O&8Fp(TX1uy_EhAFXRM;;F_H99mgIx#pdH8(UdG&r-m9!f!@ W1b`5u1aKg;1hD*%qQD@gKmlN3hZIEs diff --git a/requirements.txt b/requirements.txt index 7db9aa5..aca56b3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ email-validator python-dotenv werkzeug==2.2.3 flask-sqlalchemy==3.0.5 -openai==1.3.0 +openai==1.15.0 requests==2.31.0 flask-cors==4.0.0 gunicorn==21.2.0 diff --git a/run.py b/run.py deleted file mode 100755 index 7657b40..0000000 --- a/run.py +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env python3 -import os -import sys -from pathlib import Path -from dotenv import load_dotenv -from init_db import init_database -from app import app - -if __name__ == "__main__": - # Lade .env-Datei explizit - env_path = Path(__file__).parent / ".env" - if env_path.exists(): - print(f"Lade Umgebungsvariablen aus {env_path}") - load_dotenv(dotenv_path=env_path, override=True, force=True) - else: - print("Warnung: .env-Datei nicht gefunden!") - - # Check if CSS file exists, build it if it doesn't - css_file = Path(__file__).parent / "static" / "css" / "main.css" - if not css_file.exists(): - print("CSS file not found. Building with Tailwind...") - try: - from build_css import build_css - build_css() - except Exception as e: - print(f"Warning: Failed to build CSS: {e}") - print("You may need to run 'python build_css.py' manually.") - - # Initialize the database first - init_database() - - # Run the Flask application - app.run(debug=True, host='0.0.0.0', port=5000) \ No newline at end of file diff --git a/static/css/all.min.css b/static/css/all.min.css new file mode 100644 index 0000000..3757245 --- /dev/null +++ b/static/css/all.min.css @@ -0,0 +1,27 @@ +/* + * Font Awesome 6.4.0 + * + * This is a placeholder file. For production, you should: + * 1. Download Font Awesome from https://fontawesome.com/download + * 2. Extract the downloaded package + * 3. Copy the 'css/all.min.css' file to this location + * 4. Copy the 'webfonts' folder to '/static/webfonts/' + * + * Alternatively, you can install via npm and copy the files: + * npm install @fortawesome/fontawesome-free + * cp -r node_modules/@fortawesome/fontawesome-free/css/all.min.css static/css/ + * cp -r node_modules/@fortawesome/fontawesome-free/webfonts/ static/ + */ + +/* Placeholder styles for common Font Awesome icons */ +.fa, .fas, .far, .fab { + display: inline-block; + width: 1em; + text-align: center; +} + +/* Warning message */ +body::before { + content: "Font Awesome CSS placeholder. Please replace with the actual file."; + display: none; +} \ No newline at end of file diff --git a/static/css/assistant.css b/static/css/assistant.css index 9c2b3d3..22e9eea 100644 --- a/static/css/assistant.css +++ b/static/css/assistant.css @@ -1,21 +1,25 @@ -/* ChatGPT Assistent Styles */ +/* ChatGPT Assistent Styles - Verbesserte Version */ #chatgpt-assistant { font-family: 'Inter', sans-serif; } #assistant-chat { - transition: max-height 0.3s ease, opacity 0.3s ease; - box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.2); + transition: max-height 0.4s cubic-bezier(0.25, 0.1, 0.25, 1), + opacity 0.3s cubic-bezier(0.25, 0.1, 0.25, 1); + box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.25); border-radius: 0.75rem; overflow: hidden; + max-width: calc(100vw - 2rem); } #assistant-toggle { - transition: transform 0.3s ease; + transition: transform 0.3s ease, background-color 0.2s ease; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + z-index: 60; } #assistant-toggle:hover { - transform: scale(1.1); + transform: scale(1.1) rotate(10deg); } #assistant-history { @@ -40,27 +44,74 @@ 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; } -/* Verbesserter Glassmorphism-Effekt */ +/* Verbesserte Glassmorphism-Effekt */ .glass-morphism { - background: rgba(255, 255, 255, 0.2); - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); + background: rgba(255, 255, 255, 0.25); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.15); - border: 1px solid rgba(255, 255, 255, 0.18); + border: 1px solid rgba(255, 255, 255, 0.2); } .dark .glass-morphism { - background: rgba(15, 23, 42, 0.3); - border: 1px solid rgba(255, 255, 255, 0.05); + background: rgba(15, 23, 42, 0.35); + border: 1px solid rgba(255, 255, 255, 0.06); box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.4); } -/* Dunkleres Dark Theme */ +/* Verbesserte Farbpalette für Dark Theme */ .dark { --tw-bg-opacity: 1; background-color: rgba(10, 15, 25, var(--tw-bg-opacity)) !important; @@ -82,6 +133,54 @@ background-color: rgba(23, 33, 64, var(--tw-bg-opacity)) !important; } +/* Typing Indicator Animation Styles */ +.typing-indicator { + display: flex; + align-items: center; +} + +.typing-indicator span { + height: 8px; + width: 8px; + background-color: #888; + border-radius: 50%; + display: inline-block; + margin: 0 2px; + opacity: 0.4; + animation: bounce 1.4s infinite ease-in-out; +} + +.typing-indicator span:nth-child(1) { animation-delay: 0s; } +.typing-indicator span:nth-child(2) { animation-delay: 0.2s; } +.typing-indicator span:nth-child(3) { animation-delay: 0.4s; } + +@keyframes bounce { + 0%, 80%, 100% { transform: translateY(0); } + 40% { transform: translateY(-8px); } +} + +/* Chat Input Fokus-Effekt */ +#assistant-chat input:focus { + border-color: var(--primary-500, #3B82F6); + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.25); +} + +.dark #assistant-chat input:focus { + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.4); +} + +/* Verbesserte Responsive Layouts */ +@media (max-width: 640px) { + #assistant-chat { + width: calc(100vw - 2rem) !important; + } + + #chatgpt-assistant { + right: 1rem; + bottom: 1rem; + } +} + /* Footer immer unten */ html, body { height: 100%; diff --git a/static/css/tailwind.min.css b/static/css/tailwind.min.css new file mode 100644 index 0000000..b8d8f8c --- /dev/null +++ b/static/css/tailwind.min.css @@ -0,0 +1,16 @@ +/* + * Tailwind CSS v3.4.16 + * + * This is a placeholder file. For production, you should: + * 1. Install Tailwind CSS as a PostCSS plugin: https://tailwindcss.com/docs/installation + * 2. Run the Tailwind CLI to compile this file with your custom configuration + * 3. Replace this file with the compiled CSS + * + * The actual file should be generated using: + * npx tailwindcss -i ./src/input.css -o ./static/css/tailwind.min.css --minify + */ + +/* Base Tailwind imports */ +@tailwind base; +@tailwind components; +@tailwind utilities; \ No newline at end of file diff --git a/static/d3-extensions.js b/static/d3-extensions.js index da9c1f3..4b88dd7 100644 --- a/static/d3-extensions.js +++ b/static/d3-extensions.js @@ -450,6 +450,76 @@ 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 diff --git a/static/fonts/inter.css b/static/fonts/inter.css new file mode 100644 index 0000000..8da757d --- /dev/null +++ b/static/fonts/inter.css @@ -0,0 +1,35 @@ +/* Inter font - Local version */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 300; + src: url('../fonts/inter-light.woff2') format('woff2'); +} + +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 400; + src: url('../fonts/inter-regular.woff2') format('woff2'); +} + +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 500; + src: url('../fonts/inter-medium.woff2') format('woff2'); +} + +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 600; + src: url('../fonts/inter-semibold.woff2') format('woff2'); +} + +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 700; + src: url('../fonts/inter-bold.woff2') format('woff2'); +} \ No newline at end of file diff --git a/static/fonts/jetbrains-mono.css b/static/fonts/jetbrains-mono.css new file mode 100644 index 0000000..d6b101c --- /dev/null +++ b/static/fonts/jetbrains-mono.css @@ -0,0 +1,21 @@ +/* JetBrains Mono font - Local version */ +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 400; + src: url('../fonts/jetbrainsmono-regular.woff2') format('woff2'); +} + +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 500; + src: url('../fonts/jetbrainsmono-medium.woff2') format('woff2'); +} + +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 700; + src: url('../fonts/jetbrainsmono-bold.woff2') format('woff2'); +} \ No newline at end of file diff --git a/static/js/alpine.min.js b/static/js/alpine.min.js new file mode 100644 index 0000000..e03fbff --- /dev/null +++ b/static/js/alpine.min.js @@ -0,0 +1,12 @@ +/* + * Alpine.js v3.12.3 + * + * This is a placeholder file. For production, you should: + * 1. Download the official Alpine.js file from https://github.com/alpinejs/alpine/releases + * 2. Replace this file with the downloaded version + * + * Alternatively, you can run: + * curl -L https://cdn.jsdelivr.net/npm/alpinejs@3.12.3/dist/cdn.min.js > alpine.min.js + */ + +console.error('This is a placeholder for Alpine.js. Please replace with the actual library file.'); \ No newline at end of file diff --git a/static/js/main.js b/static/js/main.js index 27c8e80..3cdb27c 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -226,4 +226,7 @@ const MindMap = { }; // Globale Export für andere Module -window.MindMap = MindMap; \ No newline at end of file +window.MindMap = MindMap; + +// Export als Modul +export default MindMap; \ No newline at end of file diff --git a/static/js/modules/chatgpt-assistant.js b/static/js/modules/chatgpt-assistant.js index 8f1f9a2..e45e11b 100644 --- a/static/js/modules/chatgpt-assistant.js +++ b/static/js/modules/chatgpt-assistant.js @@ -11,6 +11,62 @@ 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 '
'; + return `

${line}

`; + }).join(''); + } + }; + } + } else { + // Marked ist bereits geladen + this.markdownParser = window.marked; + } } /** @@ -24,7 +80,16 @@ class ChatGPTAssistant { this.setupEventListeners(); // Ersten Willkommensnachricht anzeigen - this.addMessage("assistant", "Frage den KI-Assistenten"); + this.addMessage("assistant", "Hallo! Ich bin dein KI-Assistent (4o-mini) und habe Zugriff auf die Wissensdatenbank. Wie kann ich dir helfen?\n\nDu kannst mir Fragen über:\n- **Gedanken** in der Datenbank\n- **Kategorien** und Wissenschaftsbereiche\n- **Mindmaps** und Wissensverknüpfungen\n\nstellen."); + + // Vorschläge anzeigen + this.showSuggestions([ + "Zeige mir Gedanken zur künstlichen Intelligenz", + "Welche Kategorien gibt es in der Datenbank?", + "Suche nach Mindmaps zum Thema Informatik" + ]); + + console.log('KI-Assistent initialisiert!'); } /** @@ -45,7 +110,7 @@ class ChatGPTAssistant { // Chat-Container 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 sm:w-96 max-h-0 opacity-0'; + chatContainer.className = 'bg-white dark:bg-dark-800 rounded-lg shadow-xl overflow-hidden transition-all duration-300 w-80 md:w-96 max-h-0 opacity-0'; // Chat-Header const header = document.createElement('div'); @@ -53,7 +118,7 @@ class ChatGPTAssistant { header.innerHTML = `
- KI-Assistent + KI-Assistent (4o-mini)
- - + @@ -673,7 +511,7 @@
-
{{ user.thoughts_count|default('42') }}
+
{{ stats.thought_count if stats and stats.thought_count else 0 }}
Gedanken
@@ -682,7 +520,7 @@
-
{{ user.connections_count|default('128') }}
+
{{ stats.connections_count if stats and stats.connections_count else 0 }}
Verbindungen
@@ -691,7 +529,7 @@
-
{{ user.followers_count|default('567') }}
+
{{ stats.followers_count if stats and stats.followers_count else 0 }}
Follower
@@ -700,7 +538,7 @@
-
{{ user.contributions_count|default('89') }}
+
{{ stats.contributions_count if stats and stats.contributions_count else 0 }}
Beiträge
@@ -709,7 +547,7 @@
-
{{ user.rating|default('4.8') }}
+
{{ stats.rating if stats and stats.rating else '0.0' }}
Bewertung
@@ -731,115 +569,124 @@
- -
-
-
Neuer Gedanke hinzugefügt
-
vor 2 Stunden
-
-
-

Ich habe einen neuen Gedanken zum Thema "Künstliche Intelligenz und Kreativität" hinzugefügt. Wie können KI-Tools uns dabei helfen, kreativer zu denken?

-
-