234 Commits

Author SHA1 Message Date
f0d13cae78 feat: update logging format in app.log for better readability 2025-06-01 15:14:48 +01:00
a60b5c31ca chore: update compiled Python files in __pycache__ directories 2025-06-01 14:33:16 +01:00
f5c2e70a11 feat: Implementierung von Benachrichtigungen und sozialen Funktionen; Hinzufügen von API-Endpunkten für Benachrichtigungen, Benutzer-Follows und soziale Interaktionen; Verbesserung des Logging-Systems zur besseren Nachverfolgbarkeit von Systemereignissen. 2025-05-28 22:08:56 +02:00
1f4394e9b6 fix: update log format in app.log for better readability 2025-05-22 12:55:28 +01:00
37c457ca3f Kategorien- Button Funktion 2025-05-22 12:15:50 +01:00
936d983cb3 style: update mindmap.css for improved layout and design consistency 2025-05-22 12:02:17 +01:00
9ed9adfeaf 🔧 fix: update database and log files for improved stability 2025-05-20 10:11:07 +01:00
9c1475844c Merge branch 'main' of https://git.clickcandit.com/marwinm/website 2025-05-20 09:21:54 +01:00
310d0af0d1 feat: update logging format in app.log for better readability 2025-05-20 09:20:49 +01:00
cab8d28aeb 🔧 chore: Protokollaktualisierungen zur Dokumentation des Anwendungsstarts in app.log; mehrere Startmeldungen hinzugefügt für verbesserte Nachverfolgbarkeit. 2025-05-17 18:33:06 +02:00
9dc44f94f6 feat: update mindmap.js for improved performance and functionality 2025-05-17 10:25:40 +01:00
5b9ae85453 feat: Erweiterung der Mindmap-Funktionalität durch Verbesserung der Ladeanimation, Fehlerbehandlung und CSS-Anpassungen; Protokollaktualisierungen zur Fehlerverfolgung und Optimierung der Benutzeroberfläche. 2025-05-16 21:02:09 +01:00
302d5213ef feat: Erweiterung der Mindmap-Funktionalität durch Hinzufügen von Hover-Events für Knoten und Verbesserung der Zoom-Steuerung; Info-Panel für Knotendetails implementiert. 2025-05-16 20:40:53 +01:00
bb3211ab3d feat: enhance mindmap functionality and update UI components 2025-05-16 20:35:01 +01:00
2a246ee063 feat: Verbesserung der Mindmap-Funktionalität durch Einführung von Unterseiten und Anpassung des Designs in update_mindmap.js; Protokollaktualisierungen und Cache-Optimierungen vorgenommen. 2025-05-16 20:29:48 +01:00
fc8861c73c wir haben unterkategorien 2025-05-16 20:14:51 +01:00
8c49e7396e feat: enhance mindmap update functionality in update_mindmap.js 2025-05-16 20:04:36 +01:00
8e3c81fd06 🔧 chore: update cached Python files and logs for better performance 2025-05-16 15:02:44 +01:00
f18d23cfea 🔧 fix: update compiled Python cache and log file for better performance 2025-05-16 14:04:21 +01:00
d3405a7031 chore: Aktualisierung der Mindmap-Funktionalität und Integration von CSS/JS-Templates 2025-05-14 13:58:51 +02:00
5793902e47 chore: Änderungen commited 2025-05-14 13:56:11 +02:00
e73ccd7e80 "Update mindmap template with Convention-Format: templates/templates/mindmap.html" 2025-05-14 13:53:57 +02:00
e6784b712d 🎉 feat: "Add Minducture mindmap CSS and JS template integration" 2025-05-14 13:51:16 +02:00
35b5f321d4 chore: Änderungen commited 2025-05-14 13:48:24 +02:00
b68f65cc76 "Update mindmap Mindmap functionality 2025-05-14 13:47:35 +02:00
3a2f721f63 chore: Änderungen commited 2025-05-14 13:44:48 +02:00
5933195196 "Refactor Mindate mindmap UI updates for mindmap controls and components" (feat) 2025-05-14 13:41:24 +02:00
beccfa25a6 chore: Änderungen commited 2025-05-14 13:37:29 +02:00
bc5cef3ba8 "feat: Implement mindmap control enhancements and controls updates" 2025-05-14 12:51:53 +02:00
b867af9c8b chore: Änderungen commited 2025-05-14 12:47:02 +02:00
ee04432a49 chore: Änderungen commited 2025-05-14 12:43:13 +02:00
bbcee7f610 chore: Änderungen commited 2025-05-14 12:39:23 +02:00
1eb47fc230 chore: Änderungen commited 2025-05-14 12:15:57 +02:00
2921c5a824 chore: Änderungen commited 2025-05-14 12:13:19 +02:00
c98e238841 chore: Änderungen commited 2025-05-14 12:07:57 +02:00
af30a208ca chore: Änderungen commited 2025-05-14 12:02:15 +02:00
2e2f35ccc1 chore: Änderungen commited 2025-05-14 11:58:31 +02:00
fd293e53e1 chore: Änderungen commited 2025-05-14 11:55:58 +02:00
2b19cb000b chore: Änderungen commited 2025-05-14 11:39:58 +02:00
3aefe6c5e6 chore: Änderungen commited 2025-05-14 11:23:33 +02:00
c7b87dc643 chore: Änderungen commited 2025-05-14 11:21:10 +02:00
dc96252013 feat: enhance mindmap functionality with improved error handling and editing features; update logging for better debugging insights 2025-05-12 20:48:26 +02:00
ab56f44ae9 feat: update mindmap styles and improve node and edge rendering logic for better visualization 2025-05-12 20:36:12 +02:00
61124f5266 🗑️ chore: remove unused database and routing scripts; update cached Python bytecode files in __pycache__ directories 2025-05-12 20:31:20 +02:00
fab8d10f03 🔧 chore: Aktualisierung der Protokollierung mit zusätzlichen 404-Fehlern und Anwendungsstartmeldungen in app.log; Aktualisierung der Bytecode-Datei in __pycache__ 2025-05-12 19:54:41 +01:00
dec30e4681 feat: update app logic and improve mindmap functionality 2025-05-12 19:27:18 +01:00
a1bd999c6a feat: update app logic and improve mindmap functionality 2025-05-12 18:56:49 +01:00
b1d33ce643 feat: update app logic and improve logging functionality 2025-05-12 18:26:33 +01:00
293f877017 fix: update log file and remove unnecessary bytecode cache 2025-05-12 17:56:27 +01:00
e86d0b0f90 feat: update logging format in app.log for better readability 2025-05-11 14:05:59 +01:00
059fd167d6 🔧 chore: update cached Python bytecode files in __pycache__ directories 2025-05-11 13:30:26 +01:00
256d38e140 chore: Aktualisierung der Protokollierung mit zusätzlichen Fehlern 404 und Anwendungsstartmeldungen in app.log 2025-05-11 03:12:59 +02:00
4b75489631 chore: Erweiterung der Protokollierung mit zusätzlichen Startmeldungen der Anwendung in app.log 2025-05-11 01:13:33 +02:00
cb95c78276 "Refactor logging and improved app logging000_feat: Update code refactoring for enhanced performance enhancement in app module" 2025-05-11 00:31:29 +02:00
00cb100467 chore: Änderungen commited 2025-05-11 00:25:50 +02:00
8c66461dc8 chore: Änderungen commited 2025-05-11 00:23:27 +02:00
566f84fc0c "Refactor code cache for app.pycache__/app.c python file (feat)" 2025-05-11 00:20:49 +02:00
07eae42ba3 chore: Änderungen commited 2025-05-11 00:16:30 +02:00
0a1bebd862 "Update log file path for Conventionalenhanced logging: Migrations" 2025-05-11 00:13:40 +02:00
59b79b3466 chore: Änderungen commited 2025-05-11 00:04:49 +02:00
6f5526b648 📝 "Ref (Feature) Log Rotation) Logs now implemented for log rotation logs in app. 2025-05-11 00:01:50 +02:00
21148f0c0e chore: Änderungen commited 2025-05-10 23:56:26 +02:00
ba6cac32a9 "Refactor app. Implementing new feature for user authentication (feat): Add API endpoint enhancement)" 2025-05-10 23:53:54 +02:00
be767e9f27 Fehlerprotokollierung aktualisiert: Mehrere 404-Fehler für nicht gefundene URLs hinzugefügt, einschließlich detaillierter Rückverfolgungen und Benutzerinformationen. Diese Änderungen verbessern die Nachverfolgbarkeit von Anwendungsfehlern und unterstützen die Fehlerbehebung. 2025-05-10 23:52:41 +02:00
6aaf073ffb "Update log file structure for better logging and log rotation" 2025-05-10 23:45:42 +02:00
b6080f96cf chore: Änderungen commited 2025-05-10 23:40:37 +02:00
9ebf4b7abd "Refactor log rotation: update logs/logs/app.log, purge rotated to simplify and deleted postcss 2025-05-10 23:40:18 +02:00
5d35983f15 chore: Änderungen commited 2025-05-10 23:36:28 +02:00
7278ece2b8 "feat: Add PostCSS 2025-05-10 23:36:23 +02:00
f677e98795 "Refactor database schema for user authentication and data migration" (feat: feat or fix) 2025-05-10 23:29:42 +02:00
40c3f6d9b4 📝 "Refactor: Update log rotation correction in logging update to app.log files 2025-05-10 23:26:14 +02:00
9939db731b chore: Änderungen commited 2025-05-10 23:23:32 +02:00
d0f32a8355 chore: Änderungen commited 2025-05-10 23:20:10 +02:00
02d1801fc9 "Refactor app log file path update" 2025-05-10 23:17:13 +02:00
c51a8e23ca chore: Aktualisierung der Codebasis und Verbesserung der Struktur für bessere Wartbarkeit 2025-05-10 23:15:45 +02:00
1600647bc4 chore: Änderungen commited 2025-05-10 23:15:08 +02:00
82d03f6c48 "Refactor app. Update import/Improve Python Cython code formatting (#__" 2025-05-10 23:12:51 +02:00
d1352286b7 chore: Änderungen commited 2025-05-10 23:10:31 +02:00
e7b3374c53 feat: Hinzufügen von CSS-Stilen für die Suchergebnisse und deren Darstellung in der Benutzer-Mindmap-Vorlage zur Verbesserung der Benutzeroberfläche 2025-05-10 23:05:44 +02:00
4bf046c657 feat: Hinzufügen einer Suchleiste und Import-/Export-Buttons zur Benutzer-Mindmap-Vorlage für verbesserte Benutzerinteraktion 2025-05-10 23:05:27 +02:00
892a1212d9 chore: Änderungen commited 2025-05-10 23:04:52 +02:00
8440b7c30d "Refactor database connection for improved data consistency (feat" 2025-05-10 23:01:22 +02:00
74c2783b1a "Refactor app. Update import statements and for improved usability improvements (feat(feat): Implementing)" 2025-05-10 22:58:56 +02:00
fcd82eb5c9 chore: Änderungen commited 2025-05-10 22:56:49 +02:00
c654986f65 "Refactor user mind map template updates for better user experience" (feat) 2025-05-10 22:54:31 +02:00
f4ab617c59 chore: Änderungen commited 2025-05-10 22:52:11 +02:00
9c36179f29 "Refactor UI refactoring: Simplify app.c-specific components 2025-05-10 22:43:13 +02:00
f292cf1ce5 chore: Änderungen commited 2025-05-10 22:35:14 +02:00
3a20ea0282 chore: Änderungen commited 2025-05-10 22:28:19 +02:00
44986bfa23 "feat: Refactor UI update for my_my_account_account template" 2025-05-10 22:24:33 +02:00
41195a44cb chore: Änderungen commited 2025-05-10 22:20:49 +02:00
e1cd23230d "Refactor database schema for user authentication system data" 2025-05-10 22:17:26 +02:00
77095e91b6 chore: Änderungen commited 2025-05-10 22:14:16 +02:00
6322e046c5 "Feature: Integrate app and script and related files for Mindmap 2025-05-10 22:11:43 +02:00
2584bae149 chore: aktualisiere kompilierte Python-Bytecode-Datei in __pycache__ 2025-05-10 21:59:18 +01:00
c0bd7a3986 feat: add systades.db and update __pycache__ for improved performance 2025-05-10 21:58:29 +01:00
dec4a57b89 feat: enhance mindmap update functionality in app.py and update_mindmap.js 2025-05-10 21:27:57 +01:00
6a3b3a81c1 feat: verbessere neuronale Netzwerkdarstellung und Mindmap-Layout durch Anpassungen an Farben, Größen und Animationen 2025-05-10 21:19:54 +01:00
629813c486 feat(app): Aktualisierung der Datenbankinitialisierung für Flask 2.2+ Kompatibilität und Verbesserung der Initialisierungslogik 2025-05-10 22:11:33 +02:00
7cb2bf1ed0 feat: enhance mindmap and neural network background functionality 2025-05-10 20:40:13 +01:00
ed1d41d316 chore: automatic commit 2025-05-10 18:18 2025-05-10 18:18:34 +01:00
fe3cf81bc7 "Add/update or fix?" 2025-05-10 18:09:53 +02:00
2e68ae30b8 "Update .env and migration files for database schema" 2025-05-10 18:07:49 +02:00
858fdf5c44 chore: Änderungen commited 2025-05-10 18:05:14 +02:00
4948f3ad2a "Refactor database schema for database connection and data integrity improvements" 2025-05-10 18:02:27 +02:00
52954e51f1 Merge branch 'main' of https://git.clickcandit.com/marwinm/website 2025-05-10 16:04:40 +01:00
14f1356551 chore: update compiled Python bytecode files in __pycache__ directories 2025-05-10 15:49:21 +01:00
44c7183e97 feat(profile): verbessere das Profil-Layout mit neuen Tab-Stilen, verbessere die Benutzeroberfläche für Profil- und Passwortaktualisierungen und füge Animationen für Tab-Inhalte hinzu. 2025-05-10 15:56:14 +02:00
d99cae4956 chore: Änderungen commited 2025-05-10 15:42:34 +02:00
3ae5f2527c Merge branches 'main' and 'main' of https://git.clickcandit.com/marwinm/website 2025-05-10 15:35:35 +02:00
412dabd5c1 feat(mindmap): füge Icons und Farben für Kategorien hinzu, verbessere das Layout und die Darstellung der Mindmap in update_mindmap.js 2025-05-09 20:18:45 +01:00
5ade301f80 noch nicht ganz 2025-05-09 20:06:35 +01:00
118f8ed132 feat: enhance mindmap update functionality in update_mindmap.js 2025-05-09 20:00:46 +01:00
121f46df01 feat(mindmap): erweitere die Funktionalität zur Handhabung von Unterthemen und verbessere die Benutzerinteraktion durch neue Navigations- und Layout-Elemente in update_mindmap.js 2025-05-09 19:58:03 +01:00
4b0613eb6b feat(mindmap): entferne veraltete Mindmap-Module und -Dateien zur Optimierung der Codebasis und Verbesserung der Wartbarkeit 2025-05-09 19:46:26 +01:00
dd172d8596 feat(mindmap): improve performance of mind map rendering logic 2025-05-09 19:28:00 +01:00
653b3abe91 ich geh schlafen 2025-05-06 22:50:04 +01:00
ec50886145 es wird spät 2025-05-06 22:47:08 +01:00
c888dcc452 feat: enhance mindmap update functionality in update_mindmap.js 2025-05-06 22:26:28 +01:00
acceec4352 hab glaube gefixed 2025-05-06 22:24:49 +01:00
f093a6211c ich bin dran ! 2025-05-06 22:12:46 +01:00
58a5ea00bd Mindmap wird nicht initialisiert ! :( 2025-05-06 21:58:27 +01:00
aeb829e36a feat(mindmap): enhance interaction and initialization logic in mindmap files 2025-05-06 21:53:54 +01:00
49e5e19b7c feat(mindmap): aktualisiere die Mindmap-Datenstruktur mit neuen Knoten und Kanten, um die Benutzerinteraktion zu verbessern. Füge dynamische Knotenbeschreibungen hinzu und implementiere eine Funktion zur Aktualisierung der Mindmap, die das Layout optimiert und die Benutzererfahrung verbessert. 2025-05-06 21:25:17 +01:00
903e095b66 feat: enhance mindmap functionality and improve user interaction 2025-05-06 21:23:29 +01:00
2d083f5c0a feat: update mindmap template for improved layout and usability 2025-05-06 20:52:51 +01:00
cbe8dc3bd0 "Refactor database schema for database operations and db maintenance utilities improvements" 2025-05-05 07:04:37 +02:00
7c1533c20d feat: improve mindmap interaction and enhance performance in JS files 2025-05-03 20:22:59 +01:00
c285b7d8dc feat(mindmap): verbessere die Benutzerinteraktion und das visuelle Design der Mindmap. Füge dynamische Knotenbeschreibungen hinzu, aktualisiere die Farbpalette und optimiere die Zoom- und Knoteninteraktionen. Erweitere die Seitenleisten mit neuen Panels und verbessere die Animationen für ein ansprechenderes Nutzererlebnis. 2025-05-03 19:57:28 +01:00
21ddd38e13 feat(mindmap): enhance functionality for better user interaction 2025-05-03 19:52:35 +01:00
1cf7bfbf76 feat: erweitere die Mindmap-API zur dynamischen Erstellung und Anzeige wissenschaftlicher Knoten sowie zur Verbesserung der Fehlerbehandlung. Füge neues Skript zur Aktualisierung der Mindmap hinzu. 2025-05-03 19:44:18 +01:00
40b28134fc feat: entferne nicht verwendete Forum-Funktionen aus app.py und aktualisiere die Basisvorlage für die Suchfunktion 2025-05-03 19:31:34 +01:00
d5fababd49 feat: update app logic and enhance base template rendering 2025-05-03 19:19:53 +01:00
7c742debdf 🔧 chore: passe die Konfiguration des neuronalen Netzwerks an, indem die Knotenanzahl auf 10 und die Clusteranzahl auf 7 erhöht wird, um die Verteilung zu optimieren. 2025-05-03 16:00:48 +01:00
4a4271a23c feat: aktualisiere das Benutzerprofil, um grundlegende Statistiken anzuzeigen und die Handhabung von Fehlern zu verbessern, während die Datenbankstruktur berücksichtigt wird. 2025-05-03 15:37:33 +01:00
c1038b479f feat: update app.py to improve performance and optimize resource usage 2025-05-03 15:34:49 +01:00
cd0083544a feat: update profile template for improved user experience 2025-05-03 14:59:49 +01:00
a03bec2dff chore: update database and remove unused compiled Python files 2025-05-03 13:37:07 +01:00
997479581d 🔧 chore: update compiled Python bytecode files in __pycache__ directories 2025-05-03 12:39:17 +01:00
8153390e35 Verbessere das Design der Profilseite: Aktualisiere CSS-Stile für eine verbesserte Benutzeroberfläche im Light Mode, einschließlich optimierter Hintergründe, Rahmen und Schattierungen für verschiedene Elemente wie Einstellungen, Statistiken und Interaktionselemente. 2025-05-02 19:56:25 +02:00
bfa155628e "Refactor UIKern:000:2 amend template.pycache__/app.00/app.c file modifications to enhance user experience in-0000usability." 2025-05-02 19:53:04 +02:00
700a8a3b89 chore: Änderungen commited 2025-05-02 19:46:47 +02:00
808481ffe7 chore: Änderungen commited 2025-05-02 19:44:34 +02:00
e2c8cfaacf "Refactor templating templates for better" 2025-05-02 19:41:56 +02:00
78e37fa717 chore: Änderungen commited 2025-05-02 19:39:44 +02:00
b2cf50626a "Add default avatar icon for avatar" 2025-05-02 19:26:55 +02:00
7f48526315 chore: Aktualisiere die kompilierte Python-Datei im __pycache__ Verzeichnis 2025-05-02 19:24:20 +02:00
84f8a6bf31 chore: Änderungen commited 2025-05-02 19:23:38 +02:00
7003c89447 "Refactor database schema for user profiles and profile templates updated to remove obsolete db table 'systades (feat): systades.db) system data)" 2025-05-02 19:21:19 +02:00
d0821db983 chore: Änderungen commited 2025-05-02 19:18:49 +02:00
f0c4c514c4 "Refactor Mindicate mindmap Update for Mindate Mindous Mind mindmap Modifications in the Mind (feat/update to-mendments toughly, update forums ofthe mind-peed_induced.cetapane 2025-05-02 19:16:34 +02:00
304a399b85 chore: Änderungen commited 2025-05-02 19:13:14 +02:00
a5396c0d6e 🎨 style: update base styles and add user mindmap template 2025-05-02 19:06:09 +02:00
9cc4e70cba 🎨 style: erweitere die CSS-Stile für Schaltflächen und Links im Basis-Template zur Verbesserung der Benutzeroberfläche 2025-05-02 19:01:47 +02:00
a8cac08d30 feat: enhance chatgpt assistant styling and functionality improvements 2025-05-02 18:59:13 +02:00
42a7485ce1 🎨 style: update CSS and remove unused JS for improved design consistency 2025-05-02 18:57:01 +02:00
54a5ccc224 🎨 style: update neural network background CSS for improved aesthetics 2025-05-02 18:54:33 +02:00
a99f82d4cf feat: add theme toggle functionality and update related files 2025-05-02 18:52:18 +02:00
699127f41f 🎨 style: update UI and database schema for improved user experience 2025-05-02 18:49:02 +02:00
e8d356a27a 🎨 style: update base styles and templates for improved layout and design 2025-05-02 18:46:05 +02:00
daf2704253 feat: update profile template and remove compiled Python file 2025-05-02 18:42:56 +02:00
084059449f 🎨 style: update profile template and improve app.py functionality 2025-05-02 18:38:59 +02:00
c9bbc6ff25 🎨 style: update styles and layout in base templates and CSS files 2025-05-02 18:33:41 +02:00
742e3fda20 feat: enhance UI and functionality for mindmap creation and profile pages 2025-05-02 18:31:25 +02:00
54aa246b79 feat: add create_mindmap template for mind mapping functionality 2025-05-02 18:28:54 +02:00
505fb9aa47 🔄 chore: aktualisiere die Bytecode-Dateien im __pycache__-Verzeichnis und die systades.db-Datenbankdatei 2025-05-02 18:27:26 +02:00
e4e6541b8c 🎉 feat: update app logic and database schema for improved performance 2025-05-02 18:22:02 +02:00
e724181915 feat: improve user management and update styles for better UX 2025-05-02 18:19:58 +02:00
460c3f987e 🎨 style: update base styles and database schema for improved layout 2025-05-02 18:14:04 +02:00
7f33dea278 feat(database): add new systades.db instance and update existing file 2025-05-02 18:11:57 +02:00
726d9c9c70 🎉 feat(database): add user fields and password column migrations 2025-05-02 18:09:33 +02:00
81170fbd3d 🎨 style: update base styles and background for improved UI consistency 2025-05-02 18:02:00 +02:00
eff3fda1ca chore: update Python bytecode files in __pycache__ directory 2025-05-02 17:54:46 +02:00
d49b266d96 User Profil Fix Versuch 1 2025-05-02 16:48:00 +01:00
34a08c4a6a feat: aktualisiere Favicon mit neuem Design und passe die HTML-Vorlage an, um das neue Favicon zu integrieren 2025-05-02 16:17:39 +01:00
7918de1723 feat: update favicon generation and add neuron favicon image 2025-05-02 16:07:46 +01:00
a0e4cd2208 feat: implementiere Mindmap-Funktionalität mit dynamischer Datenladung und verbesserten Benutzeroberflächen-Elementen in mindmap.html und mindmap-init.js 2025-05-02 09:27:22 +01:00
2199d6007c feat: verbessere das Layout und die Benutzeroberfläche des Chatbereichs in index.html mit neuen Stilen und verbesserten Eingabefeldern 2025-05-02 09:14:01 +01:00
7fb9452d09 feat: passe die Konfiguration des neuronalen Netzwerks an, um die Knotenanzahl zu reduzieren und die Clusteranzahl zu erhöhen 2025-05-02 09:07:02 +01:00
1f3e60efde feat: update index.html template for improved layout and accessibility 2025-05-02 09:04:48 +01:00
5e97381c8f nenn mich designer 2025-05-02 08:51:40 +01:00
4c402423c0 feat: verbessere die Logik des neuronalen Netzwerk-Hintergrunds mit optimierten Animationen und vereinfachter Struktur 2025-05-02 08:40:34 +01:00
6d2595e3a6 feat: update neural network background logic for improved performance 2025-05-02 08:33:45 +01:00
29b44e5c52 Community funktioniert nicht 2025-05-02 08:25:06 +01:00
693e542d5f UTF8 in .env, FLASK DEBUG 2025-05-02 08:06:38 +01:00
4c3e476338 feat: update environment config and add community preview template 2025-05-02 08:01:23 +01:00
613c38ccb2 🔧 chore: update environment variables in .env file 2025-05-02 07:30:58 +01:00
91fdd43fe0 🔒 chore: entferne den OpenAI API-Schlüssel aus der Beispielumgebung für Sicherheitsgründe 2025-05-01 21:15:04 +01:00
f36dd5ffaa OpenAI Api Key 2025-05-01 21:10:19 +01:00
2e1c3ce8b0 Community erstellt 2025-05-01 21:04:37 +01:00
d80c4c9aec feat(models): update model structure for improved data handling 2025-05-01 20:56:05 +01:00
3b0bea959c 🔧 fix: update venv configuration for improved compatibility 2025-05-01 20:25:01 +01:00
cb3bfe0e6a LOL 2025-05-01 19:57:26 +01:00
fd63810845 feat: update environment and scripts for improved functionality 2025-05-01 16:29:57 +02:00
883973fe7b feat: update environment variables and enhance mindmap functionality 2025-05-01 16:27:40 +02:00
027e632856 feat: update example.env with new configuration settings 2025-05-01 16:24:29 +02:00
406289e54f 🎨 feat(mindmap): improve rendering performance and optimize code structure 2025-05-01 16:16:26 +02:00
71b33e6cec feat(mindmap): enhance mindmap rendering performance and responsiveness 2025-05-01 16:14:14 +02:00
c74d3164bb 🎨 feat: update mindmap templates and JS module for improved UI design 2025-05-01 16:11:42 +02:00
4982cddeef 🎉 feat: update Dockerfile and scripts for improved functionality 2025-05-01 16:05:52 +02:00
631619ccb4 feat: update docker-compose.yml for improved service configuration 2025-05-01 16:01:00 +02:00
f9881b678d 🔄 chore: aktualisiere die .env-Datei für verbesserte Konfiguration 2025-05-01 11:27:11 +01:00
259ce3cf69 feat: update Dockerfile and docker-compose for improved build process 2025-05-01 11:24:27 +01:00
9f4743eaea feat: update cached Python files and add new static image asset 2025-05-01 10:55:30 +01:00
de0f837cfd Optimierung der Projektstruktur: Entferne nicht mehr benötigte Skripte und Dateien, um die Wartbarkeit zu erhöhen und veraltete Komponenten zu beseitigen. 2025-04-30 15:51:07 +02:00
1c49ddfb19 chore: automatic commit 2025-04-30 15:49 2025-04-30 15:49:18 +02:00
46c16e5f01 chore: automatic commit 2025-04-30 15:44 2025-04-30 15:44:02 +02:00
84667bca00 chore: automatic commit 2025-04-30 15:41 2025-04-30 15:41:00 +02:00
779449559d chore: automatic commit 2025-04-30 15:38 2025-04-30 15:38:56 +02:00
721a10e861 Entferne nicht mehr benötigte Skripte: Lösche die Dateien check_schema.py, create_default_users.py, fix_user_table.py, test_app.py und windows_setup.bat, um die Projektstruktur zu optimieren und veraltete Komponenten zu entfernen. 2025-04-30 15:33:39 +02:00
a431873ca2 chore: automatic commit 2025-04-30 15:29 2025-04-30 15:29:23 +02:00
e4ab1e1bb5 chore: automatic commit 2025-04-30 12:48 2025-04-30 12:48:06 +02:00
f69356473b Entferne nicht mehr benötigte Dateien: Lösche docker-compose.yml, Dockerfile, README.md, requirements.txt, start_server.bat, start-flask-server.py, start.sh, test_server.py, sowie alle zugehörigen Datenbank- und Website-Dateien. Diese Bereinigung optimiert die Projektstruktur und entfernt veraltete Komponenten. 2025-04-30 12:34:06 +02:00
38ac13e87c chore: automatic commit 2025-04-30 12:32 2025-04-30 12:32:36 +02:00
0afb8cb6e2 Update neural network background configuration: reduce node count and connection distance, adjust glow and node colors, and modify shadow blur for improved visual clarity and performance. 2025-04-29 20:58:27 +01:00
5d282d2108 Refactor neural network background animation: streamline the code by consolidating node and connection logic, enhancing visual effects with improved glow and animation dynamics. Introduce responsive canvas resizing and optimize particle behavior for a smoother experience. 2025-04-29 20:54:24 +01:00
4aba72efa2 Merge branch 'main' of https://git.clickcandit.com/marwinm/website 2025-04-29 20:52:11 +01:00
89476d5353 w 2025-04-29 20:51:49 +01:00
0f7a33340a Update mindmap database: replace binary file with a new version to reflect recent changes in structure and data. 2025-04-27 08:56:56 +01:00
73501e7cda Add Flask server startup scripts: introduce start_server.bat for Windows and start-flask-server.py for enhanced server management. Update run.py to include logging and threaded request handling. Add test_server.py for server accessibility testing. 2025-04-25 17:09:09 +01:00
9f8eba6736 Refactor database initialization: streamline the process by removing the old init_database function, implementing a new structure for database setup, and ensuring the creation of a comprehensive mindmap hierarchy with an admin user. Update app.py to run on port 5000 instead of 6000. 2025-04-21 18:43:58 +01:00
b6bf9f387d Update mindmap database: replace binary file with a new version to incorporate recent structural and data changes. 2025-04-21 18:26:41 +01:00
d9fe1f8efc Update mindmap database: replace existing binary file with a new version, reflecting recent changes in mindmap structure and data. 2025-04-20 20:28:51 +01:00
fd7bc59851 Add user authentication routes: implement login, registration, and logout functionality, along with user profile and admin routes. Enhance mindmap API with error handling and default node creation. 2025-04-20 19:58:27 +01:00
55f1f87509 Refactor app initialization: encapsulate Flask app setup and database initialization within a create_app function, improving modularity and error handling during startup. 2025-04-20 19:54:07 +01:00
03f8761312 Update Docker configuration to change exposed port from 5000 to 6000 in Dockerfile and docker-compose.yml, ensuring consistency across the application. 2025-04-20 19:48:49 +01:00
506748fda7 Implement error handling and default node creation for mindmap routes; initialize database on first request to ensure root node exists. 2025-04-20 19:43:21 +01:00
6d069f68cd Update requirements.txt to include new dependencies for enhanced functionality and remove outdated packages for better compatibility. 2025-04-20 19:32:32 +01:00
4310239a7a Enhance Dockerfile: add system dependencies for building Python packages, update requirements.txt to remove specific version constraints, and verify installations with pip list. 2025-04-20 19:31:13 +01:00
e9fe907af0 Update requirements.txt to include email_validator==2.1.1 for improved email validation functionality. 2025-04-20 19:29:19 +01:00
0c69d9aba3 Remove unnecessary 'force' option from docker-compose.yml for cleaner configuration. 2025-04-20 19:26:44 +01:00
6da85cdece Refactor Docker setup: update docker-compose.yml to use a specific website directory for volumes, enable automatic restarts, and modify Dockerfile to clean up and copy application files more efficiently. 2025-04-20 19:25:08 +01:00
a073b09115 Update Docker configuration: change Docker Compose version to 3.8, enhance web service setup with context and dockerfile specifications, add volume and environment variables for Flask development, and modify Dockerfile to use Python 3.11 and improve file copying and command execution. 2025-04-20 19:22:08 +01:00
f1f4870989 Update dependencies in requirements.txt to specific versions for Flask, Flask-Login, Flask-SQLAlchemy, Werkzeug, and SQLAlchemy 2025-04-20 19:16:34 +01:00
5352 changed files with 889484 additions and 1752 deletions

View File

@@ -0,0 +1,34 @@
---
description:
globs:
alwaysApply: false
---
# KI-Integration
Die Anwendung integriert OpenAI für KI-Funktionalitäten:
## Konfiguration
- [app.py](mdc:app.py): OpenAI-Client-Initialisierung
- [requirements.txt](mdc:requirements.txt): OpenAI SDK als Abhängigkeit
## Endpunkte
- `/api/assistant`: Hauptendpunkt für KI-Anfragen
## Funktionalitäten
- Chatbot-Integration: Benutzer können mit einem KI-Assistenten kommunizieren
- Inhaltsanalyse: KI kann Gedanken und Konzepte analysieren
- Vorschläge: Kontextbezogene Vorschläge basierend auf dem Benutzerkontext
## Implementation
- Verwendet den OpenAI SDK für API-Aufrufe
- Kontextübergabe für konsistente Konversationen
- Streaming-Antworten für bessere Benutzererfahrung
## Konfigurationsparameter
- `OPENAI_API_KEY`: API-Schlüssel (in .env-Datei)
- Das System verwendet vorwiegend das Chat-Completion-API
## Sicherheitsmaßnahmen
- API-Schlüssel werden sicher über Umgebungsvariablen geladen
- Ratenbegrenzung und Fehlerbehandlung für API-Aufrufe
- Eingabevalidierung vor API-Anfragen

View File

@@ -0,0 +1,36 @@
---
description:
globs:
alwaysApply: false
---
# Authentifizierung und Benutzerrollen
Die Anwendung nutzt Flask-Login für das Authentifizierungssystem:
## Hauptkomponenten
- [LoginManager](mdc:app.py): Konfiguration im app.py
- [User Model](mdc:models.py): Die User-Klasse implementiert UserMixin für Flask-Login
- Passwort-Hashing: Verwendet Werkzeug Security für sichere Passwort-Speicherung
## Authentifizierungsrouten
- `/login`: Benutzeranmeldung (GET/POST)
- `/register`: Benutzerregistrierung (GET/POST)
- `/logout`: Benutzerabmeldung
## Benutzerrollen
- Reguläre Benutzer: Grundlegende Funktionen
- Administratoren (`is_admin=True`): Erweiterte Privilegien
## Zugriffskontrollen
- `@login_required`: Decorator für routenspezifischen Authentifizierungsschutz
- `@admin_required`: Benutzerdefinierter Decorator für Admin-Zugriffskontrolle
## Sitzungsverwaltung
- Tracking von Anmeldezeit (`last_login`)
- Langlebige Sitzungen für Präferenzen (z.B. Dark Mode)
- Angepasste Flash-Nachrichten
## Profilmanagement
- `/settings`: Benutzereinstellungen aktualisieren
- Passwortänderung
- Profildetails (Biografie, Avatar, etc.)

View File

@@ -0,0 +1,31 @@
---
description:
globs:
alwaysApply: false
---
# Konfiguration und Umgebungsvariablen
Die Anwendung verwendet Umgebungsvariablen für die Konfiguration:
## Konfigurationsdateien
- [.env](mdc:.env): Haupt-Umgebungsvariablen (nicht in Git)
- [example.env](mdc:example.env): Beispiel-Konfiguration als Vorlage
## Wichtige Konfigurationsparameter
- `SECRET_KEY`: Geheimer Schlüssel für Flask-Sitzungen
- `SQLALCHEMY_DATABASE_URI`: Datenbankverbindung
- `OPENAI_API_KEY`: API-Schlüssel für OpenAI-Integration
## Anwendungsinitialisierung
- [run.py](mdc:run.py): Lädt Umgebungsvariablen und startet die Anwendung
- [app.py](mdc:app.py): Konfiguriert Flask mit den geladenen Umgebungsvariablen
- [init_db.py](mdc:init_db.py): Initialisiert die Datenbank mit Beispieldaten
## Datenbank-Konfiguration
- SQLite-Datenbank im `/database`-Verzeichnis
- Automatische Erstellung der Datenbankstruktur bei Anwendungsstart
- Beispieldaten werden mit `init_database()` erstellt
## Ausführung der Anwendung
- Entwicklungsserver: `python run.py`
- In Produktion: Nutzung von Gunicorn (siehe requirements.txt)

View File

@@ -0,0 +1,31 @@
---
description:
globs:
alwaysApply: false
---
# Datenmodelle
Die Anwendung verwendet SQLAlchemy als ORM mit folgenden Hauptmodellen:
## Benutzer und Authentifizierung
- [User](mdc:models.py): Benutzermodell mit Authentifizierung und Profildaten
## Mind-Mapping und Wissensorganisation
- [Category](mdc:models.py): Wissenschaftliche Kategorien zur Organisation der Mindmap
- [MindMapNode](mdc:models.py): Knoten in der öffentlichen Mindmap
- [UserMindmap](mdc:models.py): Benutzerspezifische Mindmaps
- [UserMindmapNode](mdc:models.py): Speichert Positionen von Knoten in Benutzer-Mindmaps
- [MindmapNote](mdc:models.py): Private Notizen zu Mindmap-Elementen
## Gedanken und Inhalte
- [Thought](mdc:models.py): Gedanken und Konzepte, die in Mindmaps verknüpft werden
- [ThoughtRelation](mdc:models.py): Verknüpfungen zwischen verschiedenen Gedanken
- [ThoughtRating](mdc:models.py): Bewertungen von Gedanken durch Benutzer
- [Comment](mdc:models.py): Kommentare zu Gedanken
## Hauptbeziehungen
- Benutzer → Gedanken: 1-zu-n (Autor)
- Benutzer → MindMaps: 1-zu-n
- Gedanken ↔ MindMapNodes: n-zu-m
- Kategorien → MindMapNodes: 1-zu-n
- Gedanken ↔ Gedanken: über ThoughtRelation (gerichtete Beziehungen)

View File

@@ -0,0 +1,32 @@
---
description:
globs:
alwaysApply: false
---
# Entwicklungs-Workflow
## Grundlegende Entwicklungsschritte
1. Umgebung einrichten: Python 3.11 und Abhängigkeiten installieren
2. `.env`-Datei basierend auf `example.env` erstellen
3. Datenbank initialisieren: `python init_db.py`
4. Entwicklungsserver starten: `python run.py`
## Datenbankentwicklung
- Models in [models.py](mdc:models.py) definieren
- Migrationen bei Schemaänderungen durchführen
- Testdaten über [init_db.py](mdc:init_db.py) bereitstellen
## Anwendungsentwicklung
- Neue Routen in [app.py](mdc:app.py) hinzufügen
- Frontend-Templates in `/templates` erstellen/anpassen
- API-Endpoints für AJAX/Frontend-Integration implementieren
## Testing
- Tests mit pytest schreiben (siehe requirements.txt)
- Flask-Testumgebung für Integrationstest verwenden
## Best Practices
- Immer auf Datenbankmodelle zurückgreifen (kein Raw-SQL)
- API-Endpunkte mit Authentifizierung schützen
- Flash-Nachrichten für Benutzerrückmeldungen verwenden
- Code-Dokumentation in deutscher Sprache halten

View File

@@ -0,0 +1,41 @@
---
description:
globs:
alwaysApply: false
---
# Frontend-Struktur
Die Anwendung verwendet ein Flask-Jinja2-Template-System mit JavaScript-Erweiterungen:
## Template-Struktur
- `/templates`: Hauptverzeichnis für Jinja2-Templates
- `/templates/errors`: Fehlerseiten (404, 500, etc.)
- Layout-Templates für einheitliches Design
## Frontend-Assets
- `/static/css`: CSS-Dateien (mit Tailwind)
- `/static/css/src`: Quell-CSS-Dateien
- `/static/js`: JavaScript-Dateien
- `/static/js/modules`: Modulare JS-Komponenten
- `/static/img`: Bilder und grafische Elemente
## JavaScript-Funktionalität
- API-Integration: Asynchrone Kommunikation mit Backend
- Mindmap-Visualisierung: Interaktive Darstellung von Konzepten
- Benutzeroberflächen-Interaktivität: Drag & Drop, Tooltips, Modals
## CSS-Framework
- Tailwind CSS für responsive Design-Elemente
- TAILWIND CDN verwenden, nicht manuell build!
## Responsive Design
- Mobile-first Ansatz für verschiedene Gerätetypen
- Anpassungsfähiges Layout für verschiedene Bildschirmgrößen
## Zugänglichkeit
- Semantisches HTML für bessere Zugänglichkeit
- ARIA-Attribute für Screenreader-Unterstützung
## Internationalisierung
- Deutsche Benutzeroberfläche als Standard
- Vorbereitet für mehrsprachige Unterstützung

View File

@@ -0,0 +1,27 @@
---
description:
globs:
alwaysApply: false
---
# Projekt-Struktur (Systades)
## Hauptkomponenten
Diese Python-Flask-Webanwendung implementiert ein Mind-Mapping und Gedanken-Management System:
- [app.py](mdc:app.py): Hauptanwendungsdatei mit allen Routen und Endpunkten
- [models.py](mdc:models.py): Datenbankmodelle und Beziehungen
- [run.py](mdc:run.py): Startpunkt der Anwendung
- [init_db.py](mdc:init_db.py): Initialisiert die Datenbank mit Beispieldaten
## Projektstruktur
- `/database`: Enthält SQLite-Datenbank
- `/docs`: Dokumentation
- `/static`: Frontend-Ressourcen (CSS, JS, Bilder)
- `/templates`: Jinja2-Templates für die Webseiten
- `/utils`: Hilfsfunktionen und -klassen
## Hauptfunktionalität
- Mind-Mapping: Visualisierung von Wissen und Beziehungen
- Gedanken-Management: Erfassung und Organisation von Ideen und Konzepten
- Benutzer-Management: Registrierung, Login, Profile
- API-Endpunkte: RESTful-Schnittstellen für Frontend-Integration

43
.cursor/rules/routing.mdc Normal file
View File

@@ -0,0 +1,43 @@
---
description:
globs:
alwaysApply: false
---
# Routing und API-Endpunkte
## Hauptrouten (Webseiten)
- `/`: Startseite
- `/login`, `/register`, `/logout`: Authentifizierung
- `/mindmap`: Öffentliche Mindmap-Ansicht
- `/profile`: Benutzerprofil
- `/settings`: Benutzereinstellungen
- `/search`: Suchfunktion
- `/my_account`: Kontoübersicht
## API-Endpunkte
### Mindmap-Verwaltung
- `/api/mindmap`: Öffentliche Mindmap-Daten abrufen
- `/api/mindmap/public`: Öffentliche Mindmap abrufen
- `/api/mindmap/user/<id>`: Benutzer-Mindmap abrufen
- `/api/mindmap/<id>/add_node`: Knoten hinzufügen
- `/api/mindmap/<id>/remove_node/<node_id>`: Knoten entfernen
- `/api/mindmap/<id>/update_node_position`: Knotenposition aktualisieren
- `/api/mindmap/<id>/notes`: Notizen verwalten
### Gedanken und Inhalte
- `/api/thoughts`: Gedanken erstellen
- `/api/thoughts/<id>`: Gedanken abrufen, aktualisieren, löschen
- `/api/thoughts/<id>/bookmark`: Lesezeichen setzen/entfernen
- `/api/nodes/<id>/thoughts`: Gedanken zu einem Knoten abrufen/hinzufügen
### System und Benutzereinstellungen
- `/api/set_dark_mode`, `/api/get_dark_mode`: Erscheinungsbild-Einstellungen
- `/api/assistant`: KI-Assistent-Kommunikation
- `/api/categories`: Kategorien abrufen
- `/api/get_flash_messages`: Flash-Nachrichten für AJAX-Anfragen
## Fehlerbehandlung
- 404: Page Not Found
- 403: Forbidden
- 500: Internal Server Error
- 429: Too Many Requests

15
.env Normal file
View File

@@ -0,0 +1,15 @@
# MindMap Umgebungsvariablen
# 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
# OpenAI API
OPENAI_API_KEY=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
# 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

1
.gitignore vendored Normal file
View File

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

8
.vscode/jsconfig.json vendored Normal file
View File

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

68
.vscode/main.js vendored Normal file
View File

@@ -0,0 +1,68 @@
/// <reference types="vscode" />
// @ts-check
// API: https://code.visualstudio.com/api/references/vscode-api
// @ts-ignore
const vscode = require('vscode');
* @typedef {import('vscode').ExtensionContext} ExtensionContext
* @typedef {import('vscode').commands} commands
* @typedef {import('vscode').window} window
* @typedef {import('vscode').TextEditor} TextEditor
* @typedef {import('vscode').TextDocument} TextDocument
*/
/**
* Aktiviert die Erweiterung und registriert den Auto-Resume-Befehl
* @param {vscode.ExtensionContext} context - Der Erweiterungskontext
*/
function activate(context) {
const disposable = vscode.commands.registerCommand('extension.autoResume', () => {
const editor = vscode.window.activeTextEditor;
if (!editor) return;
const document = editor.document;
const text = document.getText();
// Track last click time to avoid multiple clicks
let lastClickTime = 0;
// Main function that looks for and clicks the resume link
function clickResumeLink() {
// Prevent clicking too frequently (3 second cooldown)
const now = Date.now();
if (now - lastClickTime < 3000) return;
// Check if text contains rate limit text
if (text.includes('stop the agent after 25 tool calls') ||
text.includes('Note: we default stop')) {
// Find the resume link position
const resumePos = text.indexOf('resume the conversation');
if (resumePos !== -1) {
vscode.window.showInformationMessage('Auto-resuming conversation...');
lastClickTime = now;
}
}
}
// Führe periodisch aus
const interval = global.setInterval(clickResumeLink, 1000);
// Speichere das Intervall in den Subscriptions
context.subscriptions.push({
dispose: () => global.clearInterval(interval)
});
// Führe die Funktion sofort aus
clickResumeLink();
});
context.subscriptions.push(disposable);
}
function deactivate() {
// Cleanup if needed
}
module.exports = {
activate,
deactivate
}

1274
COMMON_ERRORS.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,33 @@
FROM python:3.9-slim-buster
# Dockerfile
FROM python:3.11-slim
# Arbeitsverzeichnis in Container
WORKDIR /app
COPY requirements.txt requirements.txt
RUN pip install -r requirements.txt
# 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
COPY website .
# 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
CMD ["python", "app.py"]
# 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"]

197
README.md
View File

@@ -1 +1,196 @@
# MindMapProjekt - Roadmap
## Projektübersicht
Das MindMapProjekt ist eine interaktive Plattform zum Visualisieren, Erforschen und Teilen von Wissen. Das Projekt wird umfassend überarbeitet, um ein modernes, benutzerfreundliches Design und erweiterte Funktionalitäten zu bieten.
## Technischer Stack
- **Backend**: Python/Flask
- **Frontend**:
- Tailwind CSS (via CDN) für moderne UI
- SVG-Bibliotheken für Visualisierungen (D3.js)
- JavaScript/Alpine.js für interaktive Komponenten
- WebGL für animierte Hintergrundeffekte
- **Datenbank**: SQLite mit SQLAlchemy
- **KI-Integration**: OpenAI API für intelligente Assistenz
## Installation und Verwendung
### Installation
1. Repository klonen
2. Virtuelle Umgebung erstellen: `python -m venv venv`
3. Virtuelle Umgebung aktivieren:
- Windows: `venv\Scripts\activate`
- Unix/MacOS: `source venv/bin/activate`
4. Abhängigkeiten installieren: `pip install -r requirements.txt`
5. Datenbank initialisieren: `python TOOLS.py db:rebuild`
6. Admin-Benutzer erstellen: `python TOOLS.py user:admin`
7. Server starten: `python TOOLS.py server:run`
### Standardbenutzer
- **Admin-Benutzer**: Username: `admin` / Passwort: `admin`
- **Testbenutzer**: Username: `user` / Passwort: `user`
### Verwaltungswerkzeuge mit TOOLS.py
Das Projekt enthält ein zentrales Verwaltungsskript `TOOLS.py`, das verschiedene Hilfsfunktionen bietet:
#### Datenbankverwaltung
- `python TOOLS.py db:fix` - Reparieren der Datenbankstruktur
- `python TOOLS.py db:rebuild` - Datenbank neu aufbauen (löscht alle Daten!)
- `python TOOLS.py db:test` - Datenbankverbindung und Modelle testen
- `python TOOLS.py db:stats` - Datenbankstatistiken anzeigen
#### Benutzerverwaltung
- `python TOOLS.py user:list` - Alle Benutzer anzeigen
- `python TOOLS.py user:create -u USERNAME -e EMAIL -p PASSWORD [-a]` - Neuen Benutzer erstellen
- `python TOOLS.py user:admin` - Admin-Benutzer erstellen (admin/admin)
- `python TOOLS.py user:reset-pw -u USERNAME -p NEWPASSWORD` - Benutzerpasswort zurücksetzen
- `python TOOLS.py user:delete -u USERNAME` - Benutzer löschen
#### Serververwaltung
- `python TOOLS.py server:run [--host HOST] [--port PORT] [--no-debug]` - Entwicklungsserver starten
Für detaillierte Hilfe: `python TOOLS.py -h`
## Roadmap der Überarbeitung
### Phase 1: Grundlegende Infrastruktur ✅
- [x] Bestandsaufnahme des aktuellen Projekts
- [x] Erstellung der Roadmap
- [x] Aktualisierung der Abhängigkeiten
- [x] Integration von Tailwind CSS
- [x] Einrichtung der SVG-Bibliotheken (D3.js)
- [x] Favicon erstellen
- [x] Setup-Skript für einfache Installation
### Phase 2: Design-Überarbeitung ✅
- [x] Implementierung des Dark Mode
- [x] Erstellung eines modernen, minimalistischen UI mit Tech-Ästhetik
- [x] Responsive Design für alle Geräte
- [x] Gestaltung der Landing Page mit großer Typografie
- [x] Animierter Neurales Netzwerk-Hintergrund mit WebGL
### Phase 3: Mindmap-Funktionalitäten 🔄
- [x] Verbesserte Visualisierung mit SVG und D3.js
- [x] Implementierung der Mouseover-Funktion
- [x] Entwicklung der Suchfunktion für Knoten
- [x] Clustertopologie für neuronale Netzwerkdarstellung
- [x] Fehlerbehandlung für robuste Datenverarbeitung und Knotenverweise
- [x] Verbesserte Verbindungserkennung zwischen Knoten
- [ ] Tagging-System für Inhalte
- [ ] Quellenmanagement und -verlinkung
- [ ] Upload-Funktionalität an Knotenpunkten
### Phase 4: Kernseitenentwicklung
- [ ] Überarbeitung der Startseite mit neuen Features
- [ ] Entwicklung der "Wer sind wir?"-Seite
- [ ] Implementierung von Impressum und Datenschutzerklärung
- [ ] Erstellung der Kontaktseite mit FAQs
- [ ] Überarbeitung des Benutzerprofilbereichs
### Phase 5: Community-Features
- [ ] Entwicklung des Autorenbereichs
- [ ] Implementierung von Community-Bereichen für Themenbereiche
- [ ] Verbesserter Kommentarbereich
- [ ] Benutzerrechtemanagement
### Phase 6: KI-Integration
- [ ] Implementierung des Frage-Antwort-Systems
- [ ] KI-generierte Themeneinleitungen
- [ ] Intelligente Suchunterstützung
- [ ] Geführte Pfade durch Themenbereiche
- [ ] Vorgeschlagene Chat-Möglichkeiten
### Phase 7: Benutzerprofilfunktionen
- [ ] Speichern von Thematiken
- [ ] Persönliche Mindmap/Pinboard
- [ ] Beitragsmanagement
- [ ] Benutzerstatistiken und -aktivitäten
### Phase 8: Testing und Optimierung
- [ ] Umfassende Tests aller Funktionen
- [ ] Performance-Optimierung
- [ ] SEO-Implementierung
- [ ] Barrierefreiheit prüfen und verbessern
### Phase 9: Dokumentation und Einführung
- [ ] Erstellung von Benutzeranleitungen
- [ ] Entwicklerdokumentation
- [ ] Administratorenhandbuch
- [ ] Guided Tour für neue Benutzer
## Aktueller Status
- **Phase 1**: ✅ Abgeschlossen
- **Phase 2**: ✅ Abgeschlossen
- **Phase 3**: 🔄 In Bearbeitung (75% abgeschlossen)
## Aktuelle Fortschritte
- Grundlegende UI modernisiert mit Tailwind CSS und Dark Mode
- Neues Favicon für bessere visuelle Identität erstellt
- Setup-Prozess vereinfacht mit einem Shell-Skript
- Mindmap-Visualisierung komplett überarbeitet mit D3.js für eine interaktivere Erfahrung
- Responsive Design für optimale Darstellung auf allen Geräten
- Animierter neuronaler Netzwerk-Hintergrund mit WebGL implementiert
- Verbesserte neuronale Cluster-Darstellung in der MindMap-Ansicht
- Behebung von kritischen Fehlern in der Knotenvisualisierung und Verbindungserkennung
- Robustere Datenverarbeitung für Mindmap-Knoten implementiert
- Fehlerbehandlung für verschiedene API-Datenformate verbessert
## Neuronaler Netzwerk-Hintergrund
Ein wesentliches neues Feature ist der animierte Hintergrund, der ein neuronales Netzwerk simuliert:
- **WebGL-basierte Rendering-Engine** für hohe Performance
- **Dynamische Knoten und Verbindungen** mit realistischem Bewegungsverhalten
- **Neuronenfeuer-Simulation** mit Signalweiterleitung zwischen Knoten
- **Clustertopologie** für realistisches Erscheinungsbild
- **Anpassbare Farbgebung und Animationsparameter**
- **Flüssige Animationen** mit über 100 Knotenpunkten
Die Animation ist vollständig responsiv und passt sich automatisch an verschiedene Bildschirmgrößen an, ohne die Browser-Performance zu beeinträchtigen.
## Mindmap-Verbesserungen
Die Mindmap-Darstellung wurde grundlegend überarbeitet:
- **D3.js Force-Directed Graph** für intuitive Knotenpositionierung
- **Verbesserte Fehlerbehandlung** für robustere Datenverarbeitung
- **Neuronale Cluster-Gruppierung** von thematisch zusammengehörigen Inhalten
- **Glasmorphismus-Effekte** für moderne visuelle Darstellung
- **Verbesserte Hover- und Selektionseffekte**
- **Flüssige Animationen** bei Knotenauswahl und -fokussierung
## Nächste Schritte
- Fertigstellung des Tagging-Systems für Gedanken
- Verbesserung der Gedankenansicht im Mindmap-Bereich
- Implementierung von Quellenmanagement
- Überarbeitung der Startseite mit neuen Features
## Content Security Policy (CSP)
Die Anwendung implementiert eine Content Security Policy, um die Sicherheit zu erhöhen und unerwünschte externe Ressourcen zu blockieren. CSP wird in `app.py` konfiguriert und schränkt ein, welche Ressourcen geladen werden dürfen.
### Aktualisierung (06.06.2024)
Die Anwendung verwendet nun die Tailwind CSS CDN für vereinfachte Entwicklung. Die CSP wurde entsprechend angepasst, um die Domain `cdn.tailwindcss.com` zu erlauben.
### Lokale und CDN-Ressourcen
Die Anwendung nutzt eine Mischung aus lokalen Ressourcen und CDNs:
- **CDN-Ressourcen**:
- Tailwind CSS (cdn.tailwindcss.com)
- **Lokale Ressourcen**:
- Alpine.js
- Font Awesome
- Google Fonts (Inter und JetBrains Mono)
- WebGL-Animation (neural-network-background.js)
### CSP-Nonces
Die Anwendung verwendet Nonces für Inline-Skripte. In den Templates wird `{{ csp_nonce }}` verwendet, um den Nonce-Wert einzufügen:
```html
<script nonce="{{ csp_nonce }}">
// JavaScript Code
</script>
```
*Zuletzt aktualisiert: 15.06.2024*

464
ROADMAP.md Normal file
View File

@@ -0,0 +1,464 @@
# 🚀 SysTades Social Network - Entwicklungsroadmap
## 📋 Überblick
SysTades ist jetzt ein vollwertiges Social Network für Wissensaustausch, Mindmapping und Community-Building.
## ✅ Abgeschlossene Phasen
### Phase 1: Basis Social Network ✅
- ✅ Erweiterte Benutzermodelle mit Social Features
- ✅ Posts, Kommentare, Likes, Follows System
- ✅ Benachrichtigungssystem
- ✅ Benutzerprofile mit Statistiken
- ✅ Erweiterte Navigation und UI
-**Verbessertes Logging-System mit visuellen Enhancements**
- ✅ Social Feed mit Filtering
- ✅ Mobile-responsive Design
### Phase 2: Core Features ✅
- ✅ Mindmap-Integration in Social Posts
- ✅ Gedanken-Sharing System
- ✅ Bookmark-System für Posts
- ✅ Analytics Dashboard für Benutzer
- ✅ Erweiterte Suche (Benutzer, Posts, Gedanken)
- ✅ Real-time Benachrichtigungen
- ✅ Post-Sharing und Engagement Metrics
### Phase 3: Erweiterte Social Features ✅
- ✅ Benutzerprofile mit Tabs (Posts, Gedanken, Mindmaps, Aktivität)
- ✅ Follow/Unfollow System mit UI
- ✅ Notification Center mit Filtering
- ✅ Post-Typen (Text, Gedanke, Frage, Erkenntnis)
- ✅ Sichtbarkeitseinstellungen (Öffentlich, Follower, Privat)
- ✅ Quick-Create Post Modal
### Phase 3.5: Logging & Monitoring System ✅ (NEU)
-**Erweiterte SocialNetworkLogger Klasse mit visuellen Features**
-**Farbige Konsolen-Ausgabe mit ANSI-Codes**
-**Emoji-basierte Kategorisierung für bessere Übersicht**
-**Component-spezifisches Logging (AUTH, API, DB, ERROR, etc.)**
-**Performance-Monitoring mit Zeitstempel**
-**Strukturierte JSON-Logs für externe Analyse**
-**Decorator-basierte Instrumentierung**
-**Vollständige Integration in alle App-Komponenten**
-**Ersetzung aller print-Statements durch strukturierte Logs**
## 🔄 Aktuelle Phase 4: UI/UX Verbesserungen (In Arbeit)
### UI/UX Komponenten
- ✅ Moderne Navigation mit Icons und Badges
- ✅ Dark/Light Mode Toggle
- ✅ Responsive Mobile Navigation
- ✅ Glassmorphism Design Elements
- ✅ Gradient Themes und Farbsystem
- ✅ Toast Notification System
- ⏳ Chat/Messaging System
- ⏳ Story/Status Features
- ⏳ Advanced Image/Video Upload
### Performance Optimierungen
- ⏳ Lazy Loading für Posts
- ⏳ Image Optimization
- ⏳ Caching System
- ⏳ API Rate Limiting
- ⏳ Database Indexing
## 📈 Kommende Phasen
### Phase 5: Community Features
- 🔲 Gruppen/Communities System
- 🔲 Events und Kalenderfunktion
- 🔲 Live Discussions/Chats
- 🔲 Trending Topics/Hashtags
- 🔲 User Verification System
- 🔲 Moderation Tools
### Phase 6: Advanced Features
- 🔲 AI-basierte Content Empfehlungen
- 🔲 Voice Notes und Audio Posts
- 🔲 Video Sharing und Streaming
- 🔲 Collaborative Mindmaps
- 🔲 Knowledge Graph Visualisierung
- 🔲 Advanced Analytics
### Phase 7: Monetarisierung & Skalierung
- 🔲 Premium Features
- 🔲 Creator Economy Tools
- 🔲 API für Drittanbieter
- 🔲 Mobile Apps (iOS/Android)
- 🔲 Enterprise Features
- 🔲 Advanced Security Features
### Phase 8: Integration & Ecosystem
- 🔲 External Tool Integrations
- 🔲 Learning Management System
- 🔲 Knowledge Base Integration
- 🔲 Research Tools
- 🔲 Publication System
- 🔲 Academic Collaboration Tools
## 🏗️ Technische Architektur
### Backend Stack ✅
- **Framework**: Flask mit SQLAlchemy
- **Datenbank**: SQLite (PostgreSQL für Produktion)
- **Authentifizierung**: Flask-Login
- **API**: RESTful JSON APIs
- **Logging**: **Erweiterte SocialNetworkLogger mit visuellen Features**
- **Farbige Konsolen-Ausgabe mit ANSI-Codes**
- **Emoji-basierte Kategorisierung (🔐 AUTH, 🌐 API, 💾 DB, etc.)**
- **Component-spezifisches Logging mit Performance-Monitoring**
- **JSON-strukturierte Logs für externe Analyse**
- **Decorator-basierte automatische Instrumentierung**
- **Performance**: Pagination, Caching
### Frontend Stack ✅
- **Styling**: TailwindCSS mit Custom Themes
- **JavaScript**: Vanilla JS mit ES6+ Features
- **Icons**: Font Awesome 6
- **Responsive**: Mobile-First Design
- **Interaktivität**: Alpine.js für reaktive Komponenten
### Database Schema ✅
```sql
-- Core Tables
users (erweitert mit Social Features)
social_posts (Posts System)
social_comments (Kommentar System)
notifications (Benachrichtigungssystem)
user_settings (Benutzereinstellungen)
activities (Aktivitätsverfolgung)
-- Relationship Tables
user_friendships (Freundschaftssystem)
user_follows (Follow System)
post_likes (Like System)
comment_likes (Comment Likes)
user_thought_bookmark (Bookmark System)
```
## 📊 API Endpunkte
### Social Feed APIs ✅
- `GET /api/social/posts` - Feed Posts abrufen
- `POST /api/social/posts` - Neuen Post erstellen
- `POST /api/social/posts/{id}/like` - Post liken/unliken
- `POST /api/social/posts/{id}/share` - Post teilen
- `POST /api/social/posts/{id}/bookmark` - Post bookmarken
### User Management APIs ✅
- `GET /api/social/users/{id}` - Benutzerprofil abrufen
- `GET /api/social/users/search` - Benutzer suchen
- `POST /api/social/users/{id}/follow` - Benutzer folgen/entfolgen
### Notification APIs ✅
- `GET /api/social/notifications` - Benachrichtigungen abrufen
- `POST /api/social/notifications/{id}/read` - Als gelesen markieren
- `POST /api/social/notifications/mark-all-read` - Alle als gelesen
- `DELETE /api/social/notifications/{id}` - Benachrichtigung löschen
### Analytics APIs ✅
- `GET /api/social/analytics/dashboard` - Benutzer-Analytics
- `GET /api/social/bookmarks` - Gebookmarkte Posts
## 🔒 Sicherheit & Datenschutz
### Implementierte Features ✅
- CSRF Protection
- SQL Injection Prevention
- Input Validation & Sanitization
- Session Management
- Password Hashing
- Privacy Controls (Post Visibility)
### Geplante Features
- 2FA Authentication
- Advanced Privacy Settings
- Data Export/Import
- GDPR Compliance Tools
- Content Moderation AI
## 📱 Mobile Support
### Aktuelle Features ✅
- Responsive Design
- Touch-Friendly Interface
- Mobile Navigation
- Optimized Loading
### Geplante Features
- PWA Support
- Offline Capabilities
- Push Notifications
- Native Mobile Apps
## 🎯 Leistungsziele
### Aktueller Status
- ✅ Grundlegende Performance
- ✅ Database Queries optimiert
- ✅ Frontend Responsiveness
- ✅ Strukturiertes Logging System
### Ziele für nächste Phase
- 🎯 < 200ms API Response Zeit
- 🎯 90+ Lighthouse Score
- 🎯 Skalierung auf 10k+ Benutzer
- 🎯 99.9% Uptime
## 🧪 Testing & Quality
### Implementiert
- ✅ Manuelle Testing
- ✅ Error Handling
-**Erweiterte Logging & Monitoring mit visuellen Features**
-**Farbige, kategorisierte Logs für bessere Debugging-Erfahrung**
-**Performance-Monitoring mit Zeitstempel**
-**Component-spezifische Fehlerbehandlung**
-**Strukturierte JSON-Logs für Analyse**
### Geplant
- 🔲 Automatisierte Unit Tests
- 🔲 Integration Tests
- 🔲 Performance Tests
- 🔲 Security Audits
- 🔲 Load Testing
- 🔲 **Log-basierte Alerting System**
- 🔲 **Automated Error Reporting**
## 📈 Metriken & Analytics
### User Engagement
- Posts pro Tag
- Kommentare und Likes
- Follow/Unfollow Raten
- Session Dauer
- Return User Rate
### System Performance
- API Response Zeiten
- Database Performance
- Error Rates
- User Activity Patterns
## 🛠️ Entwicklungsumgebung
### Setup Requirements ✅
```bash
# Virtual Environment
python3.11 -m venv venv
source venv/bin/activate
# Dependencies
pip install -r requirements.txt
# Database Migration
flask db upgrade
# Development Server
python3.11 app.py
```
### Development Tools ✅
- **IDE**: Cursor/VS Code
- **Version Control**: Git
- **Database**: SQLite (dev), PostgreSQL (prod)
- **Logging**: Colored Console + File Logging
- **Debug**: Flask Debug Mode
## 🌟 Innovation Features
### Einzigartige Aspekte
- 🧠 **Knowledge-First Design**: Fokus auf Wissensaustausch
- 🎨 **Mindmap Integration**: Visuelle Gedankenlandkarten
- 🔍 **Deep Search**: Semantic Search durch Inhalte
- 📊 **Learning Analytics**: Fortschritt und Erkenntnisse
- 🤝 **Collaborative Learning**: Gemeinsam Wissen erschaffen
### Zukünftige Innovationen
- 🤖 AI-Powered Knowledge Extraction
- 🎬 Interactive Learning Experiences
- 🌐 Cross-Platform Knowledge Sync
- 📚 Dynamic Knowledge Graphs
- 🧮 Algorithmic Learning Paths
---
## 📝 Aktuelle Tasks
### Hohe Priorität
1. ⏳ Chat/Messaging System implementieren
2. ⏳ Advanced Image Upload mit Preview
3. ⏳ Performance Optimierungen
4. ⏳ Mobile App Prototyp
### Mittlere Priorität
1. 🔲 Gruppen/Communities Feature
2. 🔲 Advanced Analytics Dashboard
3. 🔲 Content Moderation Tools
4. 🔲 API Rate Limiting
### Niedrige Priorität
1. 🔲 Email Benachrichtigungen
2. 🔲 Export/Import Features
3. 🔲 Advanced Search Filters
4. 🔲 Theming System
---
**Letzte Aktualisierung**: {{ current_date }}
**Version**: 2.0.0 - Social Network Release
**Status**: ✅ Fully Functional Social Platform
# 🗺️ SysTades Roadmap
## ✅ Abgeschlossen (v1.0 - v1.3)
### 🎯 Grundfunktionen
- [x] **Benutzerauthentifizierung** - Registrierung, Login, Logout
- [x] **Interaktive Mindmap** - Cytoscape.js-basierte Visualisierung
- [x] **Gedankenverwaltung** - CRUD-Operationen für Thoughts
- [x] **Kategoriesystem** - Hierarchische Wissensorganisation
- [x] **Responsive Design** - Mobile-first Ansatz
- [x] **Dark/Light Mode** - Benutzerfreundliche Themes
### 🤖 KI-Integration
- [x] **ChatGPT-Assistent** - Integrierter AI-Chat
- [x] **Intelligente Suche** - KI-gestützte Inhaltssuche
- [x] **Automatische Kategorisierung** - AI-basierte Thought-Klassifizierung
### 🎨 UI/UX Verbesserungen
- [x] **Moderne Navigation** - Glassmorphism-Design
- [x] **Animationen** - Smooth Transitions und Hover-Effekte
- [x] **Accessibility** - ARIA-Labels und Keyboard-Navigation
- [x] **Performance-Optimierung** - Lazy Loading und Caching
## 🚀 Neu implementiert (v1.4 - Social Network Update)
### 📱 Social Network Features
- [x] **Social Feed** - Instagram/Twitter-ähnlicher Feed
- [x] **Post-System** - Erstellen, Liken, Kommentieren von Posts
- [x] **Follow-System** - Benutzer folgen und entfolgen
- [x] **Discover-Seite** - Trending Posts und empfohlene Benutzer
- [x] **Benutzerprofile** - Erweiterte Profile mit Posts, Mindmaps, Gedanken
- [x] **Benachrichtigungssystem** - Likes, Kommentare, Follows
- [x] **Community-Statistiken** - Aktive Benutzer, Posts, Mindmaps
### 🧠 Erweiterte Mindmap-Features
- [x] **Kollaborative Bearbeitung** - Vorbereitung für Echtzeit-Kollaboration
- [x] **Mindmap-Export** - JSON-Export mit geplanten weiteren Formaten
- [x] **Mindmap-Sharing** - Teilen von Mindmaps in sozialen Netzwerken
- [x] **Erweiterte Toolbar** - Neue Bearbeitungsoptionen
- [x] **Vollbild-Modus** - Immersive Mindmap-Bearbeitung
- [x] **Schnelle Knoten-/Gedanken-Erstellung** - Direkt aus der Mindmap
### 🔗 Integration & Vernetzung
- [x] **Gedanken in Posts teilen** - Wissenschaftliche Inhalte im Feed
- [x] **Mindmap-Knoten teilen** - Wissensbausteine verbreiten
- [x] **Cross-Platform Navigation** - Nahtlose Übergänge zwischen Features
- [x] **Unified Search** - Suche über alle Inhaltstypen
## 🔄 In Entwicklung (v1.5)
### 🔄 Echtzeit-Features
- [ ] **Live-Kollaboration** - Mehrere Benutzer bearbeiten gleichzeitig Mindmaps
- [ ] **WebSocket-Integration** - Echtzeit-Updates für Feed und Benachrichtigungen
- [ ] **Live-Cursor** - Sehen wo andere Benutzer arbeiten
- [ ] **Änderungshistorie** - Versionskontrolle für Mindmaps
### 💬 Erweiterte Kommunikation
- [ ] **Direktnachrichten** - Private Nachrichten zwischen Benutzern
- [ ] **Gruppen-Chats** - Themenbasierte Diskussionsgruppen
- [ ] **Video-Calls** - Integrierte Videokonferenzen für Kollaboration
- [ ] **Screen-Sharing** - Bildschirm teilen während Kollaboration
## 📋 Geplant (v1.6 - v2.0)
### 📊 Analytics & Insights
- [ ] **Lernfortschritt-Tracking** - Persönliche Wissensstatistiken
- [ ] **Mindmap-Analytics** - Nutzungsstatistiken und Hotspots
- [ ] **Community-Insights** - Trending-Themen und beliebte Inhalte
- [ ] **Empfehlungsalgorithmus** - Personalisierte Inhaltsvorschläge
### 🎓 Bildungsfeatures
- [ ] **Kurssystem** - Strukturierte Lernpfade
- [ ] **Quizzes & Tests** - Wissensüberprüfung
- [ ] **Zertifikate** - Digitale Abschlüsse
- [ ] **Mentoring-System** - Experten-Schüler-Verbindungen
### 🔧 Erweiterte Tools
- [ ] **PDF-Import** - Automatische Mindmap-Generierung aus Dokumenten
- [ ] **LaTeX-Support** - Mathematische Formeln in Gedanken
- [ ] **Multimedia-Integration** - Videos, Audio, Bilder in Mindmaps
- [ ] **API für Drittanbieter** - Integration mit anderen Tools
### 🌐 Skalierung & Performance
- [ ] **Microservices-Architektur** - Bessere Skalierbarkeit
- [ ] **CDN-Integration** - Globale Content-Delivery
- [ ] **Caching-Optimierung** - Redis für bessere Performance
- [ ] **Load Balancing** - Hochverfügbarkeit
## 🔮 Vision (v2.0+)
### 🤖 Erweiterte KI
- [ ] **Personalisierte KI-Tutoren** - Individuelle Lernbegleitung
- [ ] **Automatische Mindmap-Generierung** - KI erstellt Mindmaps aus Text
- [ ] **Intelligente Verbindungen** - KI schlägt Gedankenverknüpfungen vor
- [ ] **Adaptive Lernpfade** - KI passt Inhalte an Lernstil an
### 🌍 Globale Community
- [ ] **Mehrsprachigkeit** - Internationale Benutzergemeinschaft
- [ ] **Kultureller Austausch** - Globale Wissensnetzwerke
- [ ] **Übersetzungsfeatures** - Automatische Inhaltsübersetzung
- [ ] **Regionale Communities** - Lokale Wissensgruppen
### 🔬 Forschungstools
- [ ] **Literaturverwaltung** - Integration mit wissenschaftlichen Datenbanken
- [ ] **Zitiersystem** - Automatische Quellenangaben
- [ ] **Peer-Review-System** - Wissenschaftliche Qualitätskontrolle
- [ ] **Publikationstools** - Direkte Veröffentlichung von Forschungsergebnissen
---
## 📈 Metriken & Ziele
### Technische Ziele
- **Performance**: < 2s Ladezeit für alle Seiten
- **Verfügbarkeit**: 99.9% Uptime
- **Skalierbarkeit**: 10.000+ gleichzeitige Benutzer
- **Sicherheit**: Zero-Trust-Architektur
### Community-Ziele
- **Benutzer**: 1.000+ aktive Benutzer bis Ende 2024
- **Inhalte**: 10.000+ Gedanken und 1.000+ Mindmaps
- **Engagement**: 70%+ monatliche Aktivitätsrate
- **Zufriedenheit**: 4.5+ Sterne Bewertung
---
## 🤝 Beitragen
Interessiert an der Mitarbeit? Hier sind die Bereiche, in denen wir Unterstützung suchen:
### 👨‍💻 Entwicklung
- **Frontend**: React/Vue.js Komponenten
- **Backend**: Python/Flask API-Entwicklung
- **Mobile**: React Native App
- **DevOps**: Docker, Kubernetes, CI/CD
### 🎨 Design
- **UI/UX**: Benutzeroberflächen-Design
- **Grafik**: Icons, Illustrationen, Branding
- **Animation**: Micro-Interactions und Transitions
- **Accessibility**: Barrierefreie Gestaltung
### 📝 Content
- **Dokumentation**: Technische und Benutzer-Dokumentation
- **Tutorials**: Video- und Text-Anleitungen
- **Übersetzungen**: Mehrsprachige Inhalte
- **Community**: Moderation und Support
---
*Letzte Aktualisierung: Januar 2024*
*Version: 1.4.0 - Social Network Update*

125
TOOLS.py Normal file
View File

@@ -0,0 +1,125 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
TOOLS.py - Main utility script for the website application.
This script provides a command-line interface to all utilities
for database management, user management, and server administration.
Usage:
python3 TOOLS.py [command] [options]
Available commands:
- db:fix Fix database schema
- db:rebuild Completely rebuild the database
- db:test Test database connection and models
- db:stats Show database statistics
- user:list List all users
- user:create Create a new user
- user:admin Create admin user (username: admin, password: admin)
- user:reset-pw Reset user password
- user:delete Delete a user
- server:run Run the development server
Examples:
python3 TOOLS.py db:rebuild
python3 TOOLS.py user:admin
python3 TOOLS.py server:run --port 8080
"""
import os
import sys
import argparse
from utils import (
fix_database_schema, rebuild_database, run_all_tests, print_database_stats,
list_users, create_user, reset_password, delete_user, create_admin_user,
run_development_server
)
def parse_args():
parser = argparse.ArgumentParser(
description='Website Administration Tools',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__
)
# Main command argument
parser.add_argument('command', help='Command to execute')
# Additional arguments
parser.add_argument('--username', '-u', help='Username for user commands')
parser.add_argument('--email', '-e', help='Email for user creation')
parser.add_argument('--password', '-p', help='Password for user creation/reset')
parser.add_argument('--admin', '-a', action='store_true', help='Make user an admin')
parser.add_argument('--host', help='Host for server (default: 127.0.0.1)')
parser.add_argument('--port', type=int, help='Port for server (default: 5000)')
parser.add_argument('--no-debug', action='store_true', help='Disable debug mode for server')
return parser.parse_args()
def main():
args = parse_args()
# Database commands
if args.command == 'db:fix':
fix_database_schema()
elif args.command == 'db:rebuild':
print("WARNING: This will delete all data in the database!")
confirm = input("Are you sure you want to continue? (y/n): ").lower()
if confirm == 'y':
rebuild_database()
else:
print("Aborted.")
elif args.command == 'db:test':
run_all_tests()
elif args.command == 'db:stats':
print_database_stats()
# User commands
elif args.command == 'user:list':
list_users()
elif args.command == 'user:create':
if not args.username or not args.email or not args.password:
print("Error: Username, email, and password are required.")
print("Example: python3 TOOLS.py user:create -u username -e email -p password [-a]")
sys.exit(1)
create_user(args.username, args.email, args.password, args.admin)
elif args.command == 'user:admin':
create_admin_user()
elif args.command == 'user:reset-pw':
if not args.username or not args.password:
print("Error: Username and password are required.")
print("Example: python3 TOOLS.py user:reset-pw -u username -p new_password")
sys.exit(1)
reset_password(args.username, args.password)
elif args.command == 'user:delete':
if not args.username:
print("Error: Username is required.")
print("Example: python3 TOOLS.py user:delete -u username")
sys.exit(1)
delete_user(args.username)
# Server commands
elif args.command == 'server:run':
host = args.host or '127.0.0.1'
port = args.port or 5000
debug = not args.no_debug
run_development_server(host=host, port=port, debug=debug)
else:
print(f"Unknown command: {args.command}")
print("Run 'python3 TOOLS.py -h' for usage information")
sys.exit(1)
if __name__ == '__main__':
main()

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.

3350
app.py Normal file

File diff suppressed because it is too large Load Diff

2526
app.py.bak Normal file

File diff suppressed because it is too large Load Diff

BIN
backup/archiv_0.1.zip Normal file

Binary file not shown.

5
cookies.txt Normal file
View File

@@ -0,0 +1,5 @@
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.
#HttpOnly_127.0.0.1 FALSE / FALSE 0 session .eJwlzjEOwjAMQNG7ZGaIYztJe5nKjm2BACG1dELcnUiMf3n6n7TF7sc1re_99EvabpbWZI7cikB4dsoylLrmcKXSormH-OhKoQRSAy0v3kEzDqJlSNFCg8NIW25sfYChAgryFIWxdqyskqFWtNIdiF3awiZRaq9TzGmOnIfv_xuYabLft-fLPK0hj8O_P-1dNpA.aDdoog.bmKi2y6o3HQgIk4gwDvhirnxuoM

Binary file not shown.

Binary file not shown.

BIN
database/systades.db Normal file

Binary file not shown.

BIN
database/systades.db.backup Normal file

Binary file not shown.

View File

@@ -1,7 +1,17 @@
version: "3.9"
version: '3.9'
services:
web:
build: .
image: systades_app:latest
container_name: systades_app
restart: always
env_file:
- .env
ports:
- "5000:5000"
restart: always
volumes:
- ./database:/app/database
volumes:
db_data:

69
docs/ANLEITUNG.md Normal file
View File

@@ -0,0 +1,69 @@
# Anleitung: Animierter Netzwerk-Hintergrund
Diese Anleitung erklärt, wie Sie ein Netzwerk-Bild als animierten Hintergrund für die gesamte Website einrichten können.
## Option 1: Manuelle Installation
1. Kopieren Sie das gewünschte Netzwerk-Bild (z.B. `d2efd014-1325-471f-b9a7-90d025eb81d6.png`) in die Datei `website/static/network-bg.jpg`.
Sie können dafür das beiliegende Batch-Skript verwenden:
```
copy-network-image.bat d2efd014-1325-471f-b9a7-90d025eb81d6.png
```
2. Starten Sie den Flask-Server mit dem folgenden Befehl:
```
python start-flask-server.py
```
3. Öffnen Sie die Website unter http://127.0.0.1:5000/
## Anpassung der Animation
Sie können die Animation des Netzwerk-Hintergrunds anpassen, indem Sie die Datei `website/static/network-background.js` bearbeiten. Hier sind die wichtigsten Parameter:
```javascript
let animationSpeed = 0.0005; // Geschwindigkeit der Rotation
let scaleSpeed = 0.0002; // Geschwindigkeit der Skalierung
let opacitySpeed = 0.0003; // Geschwindigkeit der Transparenzänderung
```
## Animation der Mindmap-Verbindungen
Die Verbindungen zwischen den Knoten in der Mindmap werden jetzt mit einer fließenden Animation dargestellt. Diese Animationen verbessern die Sichtbarkeit der Zusammenhänge und machen die Interaktion mit der Karte intuitiver.
### Funktionen:
1. **Animierte Linien**: Die Verbindungslinien zwischen den Knoten bewegen sich in einem fließenden Muster.
2. **Hervorhebung bei Hover**: Beim Überfahren eines Knotens oder einer Verbindung mit der Maus werden diese hervorgehoben.
3. **Kategorien-Beziehungen**: Die visuellen Verbindungen zwischen den Kategorien sind jetzt deutlicher erkennbar.
## Position des Auswahlfelds
Das Auswahlfeld auf der Karte wurde weiter nach links verschoben, sodass es vollständig sichtbar ist, wenn keine Auswahl getroffen wurde. Die Größe wurde ebenfalls angepasst, um die Lesbarkeit zu verbessern.
## Wiederherstellung des ursprünglichen Hintergrunds (optional)
Wenn Sie zum ursprünglichen Sternenhintergrund zurückkehren möchten, müssen Sie folgende Änderungen vornehmen:
1. Bearbeiten Sie die Datei `website/templates/base.html` und ersetzen Sie:
```html
<!-- Network Background Script -->
<script src="{{ url_for('static', filename='network-background.js') }}"></script>
```
mit:
```html
<!-- Three.js für den Sternenhintergrund -->
<script src="{{ url_for('static', filename='three.min.js') }}"></script>
<script src="{{ url_for('static', filename='background.js') }}"></script>
```
2. Bearbeiten Sie die Datei `website/templates/mindmap.html` und entfernen Sie die Zeile:
```html
<script src="{{ url_for('static', filename='network-animation.js') }}"></script>
```
3. Entfernen Sie den CSS-Code für `#mindmap-container::before` und die anderen netzwerkspezifischen Stile aus der Datei `website/templates/mindmap.html`.
4. Starten Sie den Flask-Server neu, um die Änderungen zu übernehmen.

Binary file not shown.

15
example.env Normal file
View File

@@ -0,0 +1,15 @@
# MindMap Umgebungsvariablen
# 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
# OpenAI API
OPENAI_API_KEY=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
# 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

322
init_db.py Normal file
View File

@@ -0,0 +1,322 @@
#!/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 models import db, User, Thought, Comment, MindMapNode, ThoughtRelation, ThoughtRating, RelationType
from models import Category, UserMindmap, UserMindmapNode, MindmapNote
db.init_app(app)
def init_db():
with app.app_context():
print("Initialisiere Datenbank...")
# Tabellen erstellen
db.create_all()
print("Tabellen wurden erstellt.")
# Standardbenutzer erstellen, falls keine vorhanden sind
if User.query.count() == 0:
print("Erstelle Standardbenutzer...")
create_default_users()
# Standardkategorien erstellen, falls keine vorhanden sind
if Category.query.count() == 0:
print("Erstelle Standardkategorien...")
create_default_categories()
# Beispiel-Mindmap erstellen, falls keine Knoten vorhanden sind
if MindMapNode.query.count() == 0:
print("Erstelle Beispiel-Mindmap...")
create_sample_mindmap()
print("Datenbankinitialisierung abgeschlossen.")
def create_default_users():
"""Erstellt Standardbenutzer für die Anwendung"""
users = [
{
'username': 'admin',
'email': 'admin@example.com',
'password': 'admin',
'role': 'admin'
},
{
'username': 'user',
'email': 'user@example.com',
'password': 'user',
'role': 'user'
}
]
for user_data in users:
password = user_data.pop('password')
user = User(**user_data)
user.set_password(password)
db.session.add(user)
db.session.commit()
print(f"{len(users)} Benutzer wurden erstellt.")
def create_default_categories():
"""Erstellt die Standardkategorien für die Mindmap"""
# Hauptkategorien
main_categories = [
{
"name": "Philosophie",
"description": "Philosophisches Denken und Konzepte",
"color_code": "#9F7AEA",
"icon": "fa-brain"
},
{
"name": "Wissenschaft",
"description": "Wissenschaftliche Disziplinen und Erkenntnisse",
"color_code": "#60A5FA",
"icon": "fa-flask"
},
{
"name": "Technologie",
"description": "Technologische Entwicklungen und Anwendungen",
"color_code": "#10B981",
"icon": "fa-microchip"
},
{
"name": "Künste",
"description": "Künstlerische Ausdrucksformen und Werke",
"color_code": "#F59E0B",
"icon": "fa-palette"
},
{
"name": "Psychologie",
"description": "Mentale Prozesse und Verhaltensweisen",
"color_code": "#EF4444",
"icon": "fa-brain"
}
]
# Hauptkategorien erstellen
category_map = {}
for cat_data in main_categories:
category = Category(**cat_data)
db.session.add(category)
db.session.flush() # ID generieren
category_map[cat_data["name"]] = category
# Unterkategorien für Philosophie
philosophy_subcategories = [
{"name": "Ethik", "description": "Moralische Grundsätze", "icon": "fa-balance-scale", "color_code": "#8B5CF6"},
{"name": "Logik", "description": "Gesetze des Denkens", "icon": "fa-project-diagram", "color_code": "#8B5CF6"},
{"name": "Erkenntnistheorie", "description": "Natur des Wissens", "icon": "fa-lightbulb", "color_code": "#8B5CF6"}
]
# Unterkategorien für Wissenschaft
science_subcategories = [
{"name": "Physik", "description": "Studie der Materie und Energie", "icon": "fa-atom", "color_code": "#3B82F6"},
{"name": "Biologie", "description": "Studie des Lebens", "icon": "fa-dna", "color_code": "#3B82F6"},
{"name": "Mathematik", "description": "Studie der Zahlen und Strukturen", "icon": "fa-square-root-alt", "color_code": "#3B82F6"}
]
# Unterkategorien für Technologie
tech_subcategories = [
{"name": "Software", "description": "Computerprogramme und Anwendungen", "icon": "fa-code", "color_code": "#059669"},
{"name": "Hardware", "description": "Physische Komponenten der Technik", "icon": "fa-microchip", "color_code": "#059669"},
{"name": "Internet", "description": "Globales Netzwerk und Web", "icon": "fa-globe", "color_code": "#059669"}
]
# Unterkategorien für Künste
arts_subcategories = [
{"name": "Musik", "description": "Klangkunst", "icon": "fa-music", "color_code": "#D97706"},
{"name": "Literatur", "description": "Geschriebene Kunst", "icon": "fa-book", "color_code": "#D97706"},
{"name": "Bildende Kunst", "description": "Visuelle Kunst", "icon": "fa-paint-brush", "color_code": "#D97706"}
]
# Unterkategorien für Psychologie
psychology_subcategories = [
{"name": "Kognition", "description": "Gedächtnisprozesse und Denken", "icon": "fa-brain", "color_code": "#DC2626"},
{"name": "Emotionen", "description": "Gefühle und emotionale Prozesse", "icon": "fa-heart", "color_code": "#DC2626"},
{"name": "Verhalten", "description": "Beobachtbares Verhalten und Reaktionen", "icon": "fa-user", "color_code": "#DC2626"}
]
# Alle Unterkategorien zu ihren Hauptkategorien hinzufügen
for subcat_data in philosophy_subcategories:
subcat = Category(**subcat_data)
subcat.parent_id = category_map["Philosophie"].id
db.session.add(subcat)
for subcat_data in science_subcategories:
subcat = Category(**subcat_data)
subcat.parent_id = category_map["Wissenschaft"].id
db.session.add(subcat)
for subcat_data in tech_subcategories:
subcat = Category(**subcat_data)
subcat.parent_id = category_map["Technologie"].id
db.session.add(subcat)
for subcat_data in arts_subcategories:
subcat = Category(**subcat_data)
subcat.parent_id = category_map["Künste"].id
db.session.add(subcat)
for subcat_data in psychology_subcategories:
subcat = Category(**subcat_data)
subcat.parent_id = category_map["Psychologie"].id
db.session.add(subcat)
db.session.commit()
print(f"{len(main_categories)} Hauptkategorien und {len(philosophy_subcategories + science_subcategories + tech_subcategories + arts_subcategories + psychology_subcategories)} Unterkategorien wurden erstellt.")
def create_sample_mindmap():
"""Erstellt eine Beispiel-Mindmap mit Knoten und Beziehungen"""
# Kategorien für die Zuordnung
categories = Category.query.all()
category_map = {cat.name: cat for cat in categories}
# Beispielknoten erstellen
nodes = [
{
'name': 'Wissensmanagement',
'description': 'Systematische Erfassung, Speicherung und Nutzung von Wissen in Organisationen.',
'color_code': '#6366f1',
'icon': 'database',
'category': category_map.get('Konzept'),
'x': 0,
'y': 0
},
{
'name': 'Mind-Mapping',
'description': 'Technik zur visuellen Darstellung von Informationen und Zusammenhängen.',
'color_code': '#10b981',
'icon': 'git-branch',
'category': category_map.get('Prozess'),
'x': 200,
'y': -150
},
{
'name': 'Cytoscape.js',
'description': 'JavaScript-Bibliothek für die Visualisierung und Manipulation von Graphen.',
'color_code': '#3b82f6',
'icon': 'code',
'category': category_map.get('Technologie'),
'x': 350,
'y': -50
},
{
'name': 'Socket.IO',
'description': 'Bibliothek für Echtzeit-Kommunikation zwischen Client und Server.',
'color_code': '#3b82f6',
'icon': 'zap',
'category': category_map.get('Technologie'),
'x': 350,
'y': 100
},
{
'name': 'Kollaboration',
'description': 'Zusammenarbeit mehrerer Benutzer an gemeinsamen Inhalten.',
'color_code': '#f59e0b',
'icon': 'users',
'category': category_map.get('Prozess'),
'x': 200,
'y': 150
},
{
'name': 'SQLite',
'description': 'Leichtgewichtige relationale Datenbank, die ohne Server-Prozess auskommt.',
'color_code': '#3b82f6',
'icon': 'database',
'category': category_map.get('Technologie'),
'x': 0,
'y': 200
},
{
'name': 'Flask',
'description': 'Leichtgewichtiges Python-Webframework für die Entwicklung von Webanwendungen.',
'color_code': '#3b82f6',
'icon': 'server',
'category': category_map.get('Technologie'),
'x': -200,
'y': 150
},
{
'name': 'REST API',
'description': 'Architekturstil für verteilte Systeme, insbesondere Webanwendungen.',
'color_code': '#10b981',
'icon': 'link',
'category': category_map.get('Konzept'),
'x': -200,
'y': -150
},
{
'name': 'Dokumentation',
'description': 'Strukturierte Erfassung und Beschreibung von Informationen und Prozessen.',
'color_code': '#ec4899',
'icon': 'file-text',
'category': category_map.get('Dokument'),
'x': -350,
'y': 0
}
]
# Knoten in die Datenbank einfügen
node_objects = {}
for node_data in nodes:
category = node_data.pop('category', None)
x = node_data.pop('x', 0)
y = node_data.pop('y', 0)
node = MindMapNode(**node_data)
if category:
node.category_id = category.id
db.session.add(node)
db.session.flush() # Generiert IDs für neue Objekte
node_objects[node.name] = node
# Beziehungen erstellen
relationships = [
('Wissensmanagement', 'Mind-Mapping'),
('Wissensmanagement', 'Kollaboration'),
('Wissensmanagement', 'Dokumentation'),
('Mind-Mapping', 'Cytoscape.js'),
('Kollaboration', 'Socket.IO'),
('Wissensmanagement', 'SQLite'),
('SQLite', 'Flask'),
('Flask', 'REST API'),
('REST API', 'Socket.IO'),
('REST API', 'Dokumentation')
]
for parent_name, child_name in relationships:
parent = node_objects.get(parent_name)
child = node_objects.get(child_name)
if parent and child:
parent.children.append(child)
db.session.commit()
print(f"{len(nodes)} Knoten und {len(relationships)} Beziehungen wurden erstellt.")
if __name__ == '__main__':
init_db()
print("Datenbank wurde erfolgreich initialisiert!")
print("Sie können die Anwendung jetzt mit 'python app.py' starten")
print("Anmelden mit:")
print(" Admin: username=admin, password=admin")
print(" User: username=user, password=user")

0
instance/logs/errors.log Normal file
View File

0
instance/logs/social.log Normal file
View File

0
logs/api.log Normal file
View File

2419
logs/app.log Normal file

File diff suppressed because it is too large Load Diff

424
logs/errors.log Normal file
View File

@@ -0,0 +1,424 @@
2025-05-28 21:29:08 | ERROR | SysTades | ERROR | Fehler 500: 405 Method Not Allowed: The method is not allowed for the requested URL.
Endpoint: /api/thoughts, Method: GET, IP: 127.0.0.1
User: 1 (admin)
Traceback (most recent call last):
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request
rv = self.dispatch_request()
^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 841, in dispatch_request
self.raise_routing_exception(req)
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 450, in raise_routing_exception
raise request.routing_exception # type: ignore
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/flask/ctx.py", line 353, in match_request
result = self.url_adapter.match(return_rule=True) # type: ignore
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/werkzeug/routing/map.py", line 619, in match
raise MethodNotAllowed(valid_methods=list(e.have_match_for)) from None
werkzeug.exceptions.MethodNotAllowed: 405 Method Not Allowed: The method is not allowed for the requested URL.
2025-05-28 21:43:40 | ERROR | SysTades | ERROR | Fehler in social_feed nach 2.83ms - Exception: AttributeError: followed_id
Traceback (most recent call last):
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/sql/base.py", line 1633, in __getattr__
return self._index[key][1]
~~~~~~~~~~~^^^^^
KeyError: 'followed_id'
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/home/core/dev/website/utils/logger.py", line 586, in wrapper
result = func(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^
File "/home/core/dev/website/app.py", line 2774, in social_feed
followed_posts = current_user.get_feed_posts(limit=100)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/dev/website/models.py", line 193, in get_feed_posts
followed_users, SocialPost.user_id == followed_users.c.followed_id
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/sql/base.py", line 1635, in __getattr__
raise AttributeError(key) from err
AttributeError: followed_id
2025-05-28 21:43:40 | ERROR | SysTades | ERROR | Fehler 500: followed_id
Endpoint: /feed, Method: GET, IP: 127.0.0.1
User: 1 (admin)
Traceback (most recent call last):
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/sql/base.py", line 1633, in __getattr__
return self._index[key][1]
~~~~~~~~~~~^^^^^
KeyError: 'followed_id'
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request
rv = self.dispatch_request()
^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 852, in dispatch_request
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/flask_login/utils.py", line 290, in decorated_view
return current_app.ensure_sync(func)(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/dev/website/utils/logger.py", line 586, in wrapper
result = func(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^
File "/home/core/dev/website/app.py", line 2774, in social_feed
followed_posts = current_user.get_feed_posts(limit=100)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/dev/website/models.py", line 193, in get_feed_posts
followed_users, SocialPost.user_id == followed_users.c.followed_id
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/sql/base.py", line 1635, in __getattr__
raise AttributeError(key) from err
AttributeError: followed_id
2025-05-28 21:43:59 | ERROR | SysTades | ERROR | Fehler in discover nach 16.89ms - Exception: AttributeError: 'AppenderQuery' object has no attribute 'contains'
Traceback (most recent call last):
File "/home/core/dev/website/utils/logger.py", line 586, in wrapper
result = func(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^
File "/home/core/dev/website/app.py", line 2800, in discover
~current_user.following.contains(User.id)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'AppenderQuery' object has no attribute 'contains'
2025-05-28 21:43:59 | ERROR | SysTades | ERROR | Fehler 500: 'AppenderQuery' object has no attribute 'contains'
Endpoint: /discover, Method: GET, IP: 127.0.0.1
User: 1 (admin)
Traceback (most recent call last):
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request
rv = self.dispatch_request()
^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 852, in dispatch_request
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/flask_login/utils.py", line 290, in decorated_view
return current_app.ensure_sync(func)(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/dev/website/utils/logger.py", line 586, in wrapper
result = func(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^
File "/home/core/dev/website/app.py", line 2800, in discover
~current_user.following.contains(User.id)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'AppenderQuery' object has no attribute 'contains'
2025-05-28 21:46:15 | ERROR | SysTades | ERROR | Fehler in social_feed nach 54.92ms - Exception: OperationalError: (sqlite3.OperationalError) near "UNION": syntax error
[SQL: SELECT anon_1.social_post_id AS anon_1_social_post_id, anon_1.social_post_content AS anon_1_social_post_content, anon_1.social_post_image_url AS anon_1_social_post_image_url, anon_1.social_post_video_url AS anon_1_social_post_video_url, anon_1.social_post_link_url AS anon_1_social_post_link_url, anon_1.social_post_link_title AS anon_1_social_post_link_title, anon_1.social_post_link_description AS anon_1_social_post_link_description, anon_1.social_post_post_type AS anon_1_social_post_post_type, anon_1.social_post_visibility AS anon_1_social_post_visibility, anon_1.social_post_is_pinned AS anon_1_social_post_is_pinned, anon_1.social_post_like_count AS anon_1_social_post_like_count, anon_1.social_post_comment_count AS anon_1_social_post_comment_count, anon_1.social_post_share_count AS anon_1_social_post_share_count, anon_1.social_post_view_count AS anon_1_social_post_view_count, anon_1.social_post_created_at AS anon_1_social_post_created_at, anon_1.social_post_updated_at AS anon_1_social_post_updated_at, anon_1.social_post_user_id AS anon_1_social_post_user_id, anon_1.social_post_shared_thought_id AS anon_1_social_post_shared_thought_id, anon_1.social_post_shared_node_id AS anon_1_social_post_shared_node_id
FROM ((SELECT social_post.id AS social_post_id, social_post.content AS social_post_content, social_post.image_url AS social_post_image_url, social_post.video_url AS social_post_video_url, social_post.link_url AS social_post_link_url, social_post.link_title AS social_post_link_title, social_post.link_description AS social_post_link_description, social_post.post_type AS social_post_post_type, social_post.visibility AS social_post_visibility, social_post.is_pinned AS social_post_is_pinned, social_post.like_count AS social_post_like_count, social_post.comment_count AS social_post_comment_count, social_post.share_count AS social_post_share_count, social_post.view_count AS social_post_view_count, social_post.created_at AS social_post_created_at, social_post.updated_at AS social_post_updated_at, social_post.user_id AS social_post_user_id, social_post.shared_thought_id AS social_post_shared_thought_id, social_post.shared_node_id AS social_post_shared_node_id
FROM social_post
WHERE social_post.user_id IN (?) ORDER BY social_post.created_at DESC
LIMIT ? OFFSET ?) UNION SELECT social_post.id AS social_post_id, social_post.content AS social_post_content, social_post.image_url AS social_post_image_url, social_post.video_url AS social_post_video_url, social_post.link_url AS social_post_link_url, social_post.link_title AS social_post_link_title, social_post.link_description AS social_post_link_description, social_post.post_type AS social_post_post_type, social_post.visibility AS social_post_visibility, social_post.is_pinned AS social_post_is_pinned, social_post.like_count AS social_post_like_count, social_post.comment_count AS social_post_comment_count, social_post.share_count AS social_post_share_count, social_post.view_count AS social_post_view_count, social_post.created_at AS social_post_created_at, social_post.updated_at AS social_post_updated_at, social_post.user_id AS social_post_user_id, social_post.shared_thought_id AS social_post_shared_thought_id, social_post.shared_node_id AS social_post_shared_node_id
FROM social_post
WHERE social_post.user_id = ?) AS anon_1 ORDER BY anon_1.social_post_created_at DESC
LIMIT ? OFFSET ?]
[parameters: (1, 100, 0, 1, 10, 0)]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
Traceback (most recent call last):
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1969, in _exec_single_context
self.dialect.do_execute(
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/default.py", line 922, in do_execute
cursor.execute(statement, parameters)
sqlite3.OperationalError: near "UNION": syntax error
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/home/core/dev/website/utils/logger.py", line 586, in wrapper
result = func(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^
File "/home/core/dev/website/app.py", line 2782, in social_feed
posts = all_posts.paginate(
^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/flask_sqlalchemy/query.py", line 98, in paginate
return QueryPagination(
^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/flask_sqlalchemy/pagination.py", line 72, in __init__
items = self._query_items()
^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/flask_sqlalchemy/pagination.py", line 358, in _query_items
out = query.limit(self.per_page).offset(self._query_offset).all()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/query.py", line 2693, in all
return self._iter().all() # type: ignore
^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/query.py", line 2847, in _iter
result: Union[ScalarResult[_T], Result[_T]] = self.session.execute(
^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 2308, in execute
return self._execute_internal(
^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 2190, in _execute_internal
result: Result[Any] = compile_state_cls.orm_execute_statement(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/context.py", line 293, in orm_execute_statement
result = conn.execute(
^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1416, in execute
return meth(
^^^^^
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/sql/elements.py", line 516, in _execute_on_connection
return connection._execute_clauseelement(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1639, in _execute_clauseelement
ret = self._execute_context(
^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1848, in _execute_context
return self._exec_single_context(
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1988, in _exec_single_context
self._handle_dbapi_exception(
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 2343, in _handle_dbapi_exception
raise sqlalchemy_exception.with_traceback(exc_info[2]) from e
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1969, in _exec_single_context
self.dialect.do_execute(
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/default.py", line 922, in do_execute
cursor.execute(statement, parameters)
sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) near "UNION": syntax error
[SQL: SELECT anon_1.social_post_id AS anon_1_social_post_id, anon_1.social_post_content AS anon_1_social_post_content, anon_1.social_post_image_url AS anon_1_social_post_image_url, anon_1.social_post_video_url AS anon_1_social_post_video_url, anon_1.social_post_link_url AS anon_1_social_post_link_url, anon_1.social_post_link_title AS anon_1_social_post_link_title, anon_1.social_post_link_description AS anon_1_social_post_link_description, anon_1.social_post_post_type AS anon_1_social_post_post_type, anon_1.social_post_visibility AS anon_1_social_post_visibility, anon_1.social_post_is_pinned AS anon_1_social_post_is_pinned, anon_1.social_post_like_count AS anon_1_social_post_like_count, anon_1.social_post_comment_count AS anon_1_social_post_comment_count, anon_1.social_post_share_count AS anon_1_social_post_share_count, anon_1.social_post_view_count AS anon_1_social_post_view_count, anon_1.social_post_created_at AS anon_1_social_post_created_at, anon_1.social_post_updated_at AS anon_1_social_post_updated_at, anon_1.social_post_user_id AS anon_1_social_post_user_id, anon_1.social_post_shared_thought_id AS anon_1_social_post_shared_thought_id, anon_1.social_post_shared_node_id AS anon_1_social_post_shared_node_id
FROM ((SELECT social_post.id AS social_post_id, social_post.content AS social_post_content, social_post.image_url AS social_post_image_url, social_post.video_url AS social_post_video_url, social_post.link_url AS social_post_link_url, social_post.link_title AS social_post_link_title, social_post.link_description AS social_post_link_description, social_post.post_type AS social_post_post_type, social_post.visibility AS social_post_visibility, social_post.is_pinned AS social_post_is_pinned, social_post.like_count AS social_post_like_count, social_post.comment_count AS social_post_comment_count, social_post.share_count AS social_post_share_count, social_post.view_count AS social_post_view_count, social_post.created_at AS social_post_created_at, social_post.updated_at AS social_post_updated_at, social_post.user_id AS social_post_user_id, social_post.shared_thought_id AS social_post_shared_thought_id, social_post.shared_node_id AS social_post_shared_node_id
FROM social_post
WHERE social_post.user_id IN (?) ORDER BY social_post.created_at DESC
LIMIT ? OFFSET ?) UNION SELECT social_post.id AS social_post_id, social_post.content AS social_post_content, social_post.image_url AS social_post_image_url, social_post.video_url AS social_post_video_url, social_post.link_url AS social_post_link_url, social_post.link_title AS social_post_link_title, social_post.link_description AS social_post_link_description, social_post.post_type AS social_post_post_type, social_post.visibility AS social_post_visibility, social_post.is_pinned AS social_post_is_pinned, social_post.like_count AS social_post_like_count, social_post.comment_count AS social_post_comment_count, social_post.share_count AS social_post_share_count, social_post.view_count AS social_post_view_count, social_post.created_at AS social_post_created_at, social_post.updated_at AS social_post_updated_at, social_post.user_id AS social_post_user_id, social_post.shared_thought_id AS social_post_shared_thought_id, social_post.shared_node_id AS social_post_shared_node_id
FROM social_post
WHERE social_post.user_id = ?) AS anon_1 ORDER BY anon_1.social_post_created_at DESC
LIMIT ? OFFSET ?]
[parameters: (1, 100, 0, 1, 10, 0)]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2025-05-28 21:46:15 | ERROR | SysTades | ERROR | Fehler 500: (sqlite3.OperationalError) near "UNION": syntax error
[SQL: SELECT anon_1.social_post_id AS anon_1_social_post_id, anon_1.social_post_content AS anon_1_social_post_content, anon_1.social_post_image_url AS anon_1_social_post_image_url, anon_1.social_post_video_url AS anon_1_social_post_video_url, anon_1.social_post_link_url AS anon_1_social_post_link_url, anon_1.social_post_link_title AS anon_1_social_post_link_title, anon_1.social_post_link_description AS anon_1_social_post_link_description, anon_1.social_post_post_type AS anon_1_social_post_post_type, anon_1.social_post_visibility AS anon_1_social_post_visibility, anon_1.social_post_is_pinned AS anon_1_social_post_is_pinned, anon_1.social_post_like_count AS anon_1_social_post_like_count, anon_1.social_post_comment_count AS anon_1_social_post_comment_count, anon_1.social_post_share_count AS anon_1_social_post_share_count, anon_1.social_post_view_count AS anon_1_social_post_view_count, anon_1.social_post_created_at AS anon_1_social_post_created_at, anon_1.social_post_updated_at AS anon_1_social_post_updated_at, anon_1.social_post_user_id AS anon_1_social_post_user_id, anon_1.social_post_shared_thought_id AS anon_1_social_post_shared_thought_id, anon_1.social_post_shared_node_id AS anon_1_social_post_shared_node_id
FROM ((SELECT social_post.id AS social_post_id, social_post.content AS social_post_content, social_post.image_url AS social_post_image_url, social_post.video_url AS social_post_video_url, social_post.link_url AS social_post_link_url, social_post.link_title AS social_post_link_title, social_post.link_description AS social_post_link_description, social_post.post_type AS social_post_post_type, social_post.visibility AS social_post_visibility, social_post.is_pinned AS social_post_is_pinned, social_post.like_count AS social_post_like_count, social_post.comment_count AS social_post_comment_count, social_post.share_count AS social_post_share_count, social_post.view_count AS social_post_view_count, social_post.created_at AS social_post_created_at, social_post.updated_at AS social_post_updated_at, social_post.user_id AS social_post_user_id, social_post.shared_thought_id AS social_post_shared_thought_id, social_post.shared_node_id AS social_post_shared_node_id
FROM social_post
WHERE social_post.user_id IN (?) ORDER BY social_post.created_at DESC
LIMIT ? OFFSET ?) UNION SELECT social_post.id AS social_post_id, social_post.content AS social_post_content, social_post.image_url AS social_post_image_url, social_post.video_url AS social_post_video_url, social_post.link_url AS social_post_link_url, social_post.link_title AS social_post_link_title, social_post.link_description AS social_post_link_description, social_post.post_type AS social_post_post_type, social_post.visibility AS social_post_visibility, social_post.is_pinned AS social_post_is_pinned, social_post.like_count AS social_post_like_count, social_post.comment_count AS social_post_comment_count, social_post.share_count AS social_post_share_count, social_post.view_count AS social_post_view_count, social_post.created_at AS social_post_created_at, social_post.updated_at AS social_post_updated_at, social_post.user_id AS social_post_user_id, social_post.shared_thought_id AS social_post_shared_thought_id, social_post.shared_node_id AS social_post_shared_node_id
FROM social_post
WHERE social_post.user_id = ?) AS anon_1 ORDER BY anon_1.social_post_created_at DESC
LIMIT ? OFFSET ?]
[parameters: (1, 100, 0, 1, 10, 0)]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
Endpoint: /feed, Method: GET, IP: 127.0.0.1
User: 1 (admin)
Traceback (most recent call last):
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1969, in _exec_single_context
self.dialect.do_execute(
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/default.py", line 922, in do_execute
cursor.execute(statement, parameters)
sqlite3.OperationalError: near "UNION": syntax error
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request
rv = self.dispatch_request()
^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 852, in dispatch_request
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/flask_login/utils.py", line 290, in decorated_view
return current_app.ensure_sync(func)(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/dev/website/utils/logger.py", line 586, in wrapper
result = func(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^
File "/home/core/dev/website/app.py", line 2782, in social_feed
posts = all_posts.paginate(
^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/flask_sqlalchemy/query.py", line 98, in paginate
return QueryPagination(
^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/flask_sqlalchemy/pagination.py", line 72, in __init__
items = self._query_items()
^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/flask_sqlalchemy/pagination.py", line 358, in _query_items
out = query.limit(self.per_page).offset(self._query_offset).all()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/query.py", line 2693, in all
return self._iter().all() # type: ignore
^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/query.py", line 2847, in _iter
result: Union[ScalarResult[_T], Result[_T]] = self.session.execute(
^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 2308, in execute
return self._execute_internal(
^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 2190, in _execute_internal
result: Result[Any] = compile_state_cls.orm_execute_statement(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/context.py", line 293, in orm_execute_statement
result = conn.execute(
^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1416, in execute
return meth(
^^^^^
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/sql/elements.py", line 516, in _execute_on_connection
return connection._execute_clauseelement(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1639, in _execute_clauseelement
ret = self._execute_context(
^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1848, in _execute_context
return self._exec_single_context(
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1988, in _exec_single_context
self._handle_dbapi_exception(
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 2343, in _handle_dbapi_exception
raise sqlalchemy_exception.with_traceback(exc_info[2]) from e
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1969, in _exec_single_context
self.dialect.do_execute(
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/default.py", line 922, in do_execute
cursor.execute(statement, parameters)
sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) near "UNION": syntax error
[SQL: SELECT anon_1.social_post_id AS anon_1_social_post_id, anon_1.social_post_content AS anon_1_social_post_content, anon_1.social_post_image_url AS anon_1_social_post_image_url, anon_1.social_post_video_url AS anon_1_social_post_video_url, anon_1.social_post_link_url AS anon_1_social_post_link_url, anon_1.social_post_link_title AS anon_1_social_post_link_title, anon_1.social_post_link_description AS anon_1_social_post_link_description, anon_1.social_post_post_type AS anon_1_social_post_post_type, anon_1.social_post_visibility AS anon_1_social_post_visibility, anon_1.social_post_is_pinned AS anon_1_social_post_is_pinned, anon_1.social_post_like_count AS anon_1_social_post_like_count, anon_1.social_post_comment_count AS anon_1_social_post_comment_count, anon_1.social_post_share_count AS anon_1_social_post_share_count, anon_1.social_post_view_count AS anon_1_social_post_view_count, anon_1.social_post_created_at AS anon_1_social_post_created_at, anon_1.social_post_updated_at AS anon_1_social_post_updated_at, anon_1.social_post_user_id AS anon_1_social_post_user_id, anon_1.social_post_shared_thought_id AS anon_1_social_post_shared_thought_id, anon_1.social_post_shared_node_id AS anon_1_social_post_shared_node_id
FROM ((SELECT social_post.id AS social_post_id, social_post.content AS social_post_content, social_post.image_url AS social_post_image_url, social_post.video_url AS social_post_video_url, social_post.link_url AS social_post_link_url, social_post.link_title AS social_post_link_title, social_post.link_description AS social_post_link_description, social_post.post_type AS social_post_post_type, social_post.visibility AS social_post_visibility, social_post.is_pinned AS social_post_is_pinned, social_post.like_count AS social_post_like_count, social_post.comment_count AS social_post_comment_count, social_post.share_count AS social_post_share_count, social_post.view_count AS social_post_view_count, social_post.created_at AS social_post_created_at, social_post.updated_at AS social_post_updated_at, social_post.user_id AS social_post_user_id, social_post.shared_thought_id AS social_post_shared_thought_id, social_post.shared_node_id AS social_post_shared_node_id
FROM social_post
WHERE social_post.user_id IN (?) ORDER BY social_post.created_at DESC
LIMIT ? OFFSET ?) UNION SELECT social_post.id AS social_post_id, social_post.content AS social_post_content, social_post.image_url AS social_post_image_url, social_post.video_url AS social_post_video_url, social_post.link_url AS social_post_link_url, social_post.link_title AS social_post_link_title, social_post.link_description AS social_post_link_description, social_post.post_type AS social_post_post_type, social_post.visibility AS social_post_visibility, social_post.is_pinned AS social_post_is_pinned, social_post.like_count AS social_post_like_count, social_post.comment_count AS social_post_comment_count, social_post.share_count AS social_post_share_count, social_post.view_count AS social_post_view_count, social_post.created_at AS social_post_created_at, social_post.updated_at AS social_post_updated_at, social_post.user_id AS social_post_user_id, social_post.shared_thought_id AS social_post_shared_thought_id, social_post.shared_node_id AS social_post_shared_node_id
FROM social_post
WHERE social_post.user_id = ?) AS anon_1 ORDER BY anon_1.social_post_created_at DESC
LIMIT ? OFFSET ?]
[parameters: (1, 100, 0, 1, 10, 0)]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2025-05-28 21:48:48 | ERROR | SysTades | ERROR | Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
Endpoint: /sw.js, Method: GET, IP: 127.0.0.1
Nicht angemeldet
Traceback (most recent call last):
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request
rv = self.dispatch_request()
^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 841, in dispatch_request
self.raise_routing_exception(req)
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 450, in raise_routing_exception
raise request.routing_exception # type: ignore
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/flask/ctx.py", line 353, in match_request
result = self.url_adapter.match(return_rule=True) # type: ignore
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/werkzeug/routing/map.py", line 624, in match
raise NotFound() from None
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
2025-05-28 21:48:54 | ERROR | SysTades | ERROR | Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
Endpoint: /static/fonts/inter-regular.woff2, Method: GET, IP: 127.0.0.1
User: 1 (admin)
Traceback (most recent call last):
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request
rv = self.dispatch_request()
^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 852, in dispatch_request
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 257, in <lambda>
view_func=lambda **kw: self_ref().send_static_file(**kw), # type: ignore # noqa: B950
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 305, in send_static_file
return send_from_directory(
^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/flask/helpers.py", line 554, in send_from_directory
return werkzeug.utils.send_from_directory( # type: ignore[return-value]
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/werkzeug/utils.py", line 574, in send_from_directory
raise NotFound()
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
2025-05-28 21:49:17 | ERROR | SysTades | ERROR | Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
Endpoint: /sw.js, Method: GET, IP: 127.0.0.1
Nicht angemeldet
Traceback (most recent call last):
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request
rv = self.dispatch_request()
^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 841, in dispatch_request
self.raise_routing_exception(req)
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 450, in raise_routing_exception
raise request.routing_exception # type: ignore
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/flask/ctx.py", line 353, in match_request
result = self.url_adapter.match(return_rule=True) # type: ignore
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/werkzeug/routing/map.py", line 624, in match
raise NotFound() from None
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
2025-05-28 21:55:55 | ERROR | SysTades | ERROR | Fehler 500: name 'follows' is not defined
Endpoint: /api/discover/users, Method: GET, IP: 127.0.0.1
User: 1 (admin)
Traceback (most recent call last):
File "/home/core/dev/website/app.py", line 424, in wrapper
return func(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^
File "/home/core/dev/website/app.py", line 3141, in discover_users
not_following_subquery = db.session.query(follows.c.followed_id).filter(
^^^^^^^
NameError: name 'follows' is not defined
2025-05-28 21:55:55 | ERROR | SysTades | ERROR | Fehler 500: name 'follows' is not defined
Endpoint: /api/discover/users, Method: GET, IP: 127.0.0.1
User: 1 (admin)
Traceback (most recent call last):
File "/home/core/dev/website/app.py", line 424, in wrapper
return func(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^
File "/home/core/dev/website/app.py", line 3141, in discover_users
not_following_subquery = db.session.query(follows.c.followed_id).filter(
^^^^^^^
NameError: name 'follows' is not defined
2025-05-28 21:56:25 | ERROR | SysTades | ERROR | Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
Endpoint: /auth/login, Method: GET, IP: 127.0.0.1
Nicht angemeldet
Traceback (most recent call last):
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request
rv = self.dispatch_request()
^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 841, in dispatch_request
self.raise_routing_exception(req)
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 450, in raise_routing_exception
raise request.routing_exception # type: ignore
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/flask/ctx.py", line 353, in match_request
result = self.url_adapter.match(return_rule=True) # type: ignore
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/werkzeug/routing/map.py", line 624, in match
raise NotFound() from None
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
2025-05-28 21:56:41 | ERROR | SysTades | ERROR | Fehler 500: name 'follows' is not defined
Endpoint: /api/discover/users, Method: GET, IP: 127.0.0.1
User: 1 (admin)
Traceback (most recent call last):
File "/home/core/dev/website/app.py", line 424, in wrapper
return func(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^
File "/home/core/dev/website/app.py", line 3141, in discover_users
not_following_subquery = db.session.query(follows.c.followed_id).filter(
^^^^^^^
NameError: name 'follows' is not defined
2025-05-28 21:57:25 | ERROR | SysTades | ERROR | Fehler 500: name 'follows' is not defined
Endpoint: /api/discover/users, Method: GET, IP: 127.0.0.1
User: 1 (admin)
Traceback (most recent call last):
File "/home/core/dev/website/app.py", line 424, in wrapper
return func(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^
File "/home/core/dev/website/app.py", line 3141, in discover_users
not_following_subquery = db.session.query(user_follows.c.followed_id).filter(
^^^^^^^
NameError: name 'follows' is not defined
2025-05-28 21:58:02 | ERROR | SysTades | ERROR | Fehler 500: name 'follows' is not defined
Endpoint: /api/discover/users, Method: GET, IP: 127.0.0.1
User: 1 (admin)
Traceback (most recent call last):
File "/home/core/dev/website/app.py", line 424, in wrapper
return func(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^
File "/home/core/dev/website/app.py", line 3141, in discover_users
users = User.query.filter(
NameError: name 'follows' is not defined

1
migrations/README Normal file
View File

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

Binary file not shown.

Binary file not shown.

50
migrations/alembic.ini Normal file
View File

@@ -0,0 +1,50 @@
# A generic, single database configuration.
[alembic]
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic,flask_migrate
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[logger_flask_migrate]
level = INFO
handlers =
qualname = flask_migrate
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

113
migrations/env.py Normal file
View File

@@ -0,0 +1,113 @@
import logging
from logging.config import fileConfig
from flask import current_app
from alembic import context
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
logger = logging.getLogger('alembic.env')
def get_engine():
try:
# this works with Flask-SQLAlchemy<3 and Alchemical
return current_app.extensions['migrate'].db.get_engine()
except (TypeError, AttributeError):
# this works with Flask-SQLAlchemy>=3
return current_app.extensions['migrate'].db.engine
def get_engine_url():
try:
return get_engine().url.render_as_string(hide_password=False).replace(
'%', '%%')
except AttributeError:
return str(get_engine().url).replace('%', '%%')
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
config.set_main_option('sqlalchemy.url', get_engine_url())
target_db = current_app.extensions['migrate'].db
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def get_metadata():
if hasattr(target_db, 'metadatas'):
return target_db.metadatas[None]
return target_db.metadata
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url, target_metadata=get_metadata(), literal_binds=True
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
# this callback is used to prevent an auto-migration from being generated
# when there are no changes to the schema
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
def process_revision_directives(context, revision, directives):
if getattr(config.cmd_opts, 'autogenerate', False):
script = directives[0]
if script.upgrade_ops.is_empty():
directives[:] = []
logger.info('No changes in schema detected.')
conf_args = current_app.extensions['migrate'].configure_args
if conf_args.get("process_revision_directives") is None:
conf_args["process_revision_directives"] = process_revision_directives
connectable = get_engine()
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=get_metadata(),
**conf_args
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

24
migrations/script.py.mako Normal file
View File

@@ -0,0 +1,24 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

View File

@@ -0,0 +1,38 @@
"""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

@@ -0,0 +1,40 @@
"""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

@@ -0,0 +1,46 @@
"""Add password column to user
Revision ID: d4406f5b12f7
Revises:
Create Date: 2025-04-28 21:26:37.430823
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'd4406f5b12f7'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.add_column(sa.Column('password', sa.String(length=512), nullable=False, server_default="changeme"))
batch_op.add_column(sa.Column('is_active', sa.Boolean(), nullable=True))
batch_op.add_column(sa.Column('role', sa.String(length=20), nullable=True))
batch_op.drop_column('last_login')
batch_op.drop_column('bio')
batch_op.drop_column('password_hash')
batch_op.drop_column('is_admin')
batch_op.drop_column('avatar')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.add_column(sa.Column('avatar', sa.VARCHAR(length=200), nullable=True))
batch_op.add_column(sa.Column('is_admin', sa.BOOLEAN(), nullable=True))
batch_op.add_column(sa.Column('password_hash', sa.VARCHAR(length=128), nullable=True))
batch_op.add_column(sa.Column('bio', sa.TEXT(), nullable=True))
batch_op.add_column(sa.Column('last_login', sa.DATETIME(), nullable=True))
batch_op.drop_column('role')
batch_op.drop_column('is_active')
batch_op.drop_column('password')
# ### end Alembic commands ###

695
models.py Normal file
View File

@@ -0,0 +1,695 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from flask_sqlalchemy import SQLAlchemy
from flask_login import UserMixin
from datetime import datetime
from werkzeug.security import generate_password_hash, check_password_hash
from enum import Enum
import uuid as uuid_pkg
import os
db = SQLAlchemy()
# Beziehungstypen für Gedankenverknüpfungen
class RelationType(Enum):
SUPPORTS = "stützt"
CONTRADICTS = "widerspricht"
BUILDS_UPON = "baut auf auf"
GENERALIZES = "verallgemeinert"
SPECIFIES = "spezifiziert"
INSPIRES = "inspiriert"
# Beziehungstabelle für viele-zu-viele Beziehung zwischen MindMapNodes
node_relationship = db.Table('node_relationship',
db.Column('parent_id', db.Integer, db.ForeignKey('mind_map_node.id'), primary_key=True),
db.Column('child_id', db.Integer, db.ForeignKey('mind_map_node.id'), primary_key=True)
)
# Beziehungstabelle für öffentliche Knoten und Gedanken
node_thought_association = db.Table('node_thought_association',
db.Column('node_id', db.Integer, db.ForeignKey('mind_map_node.id'), primary_key=True),
db.Column('thought_id', db.Integer, db.ForeignKey('thought.id'), primary_key=True)
)
# Beziehungstabelle für Benutzer-spezifische Mindmap-Knoten und Gedanken
user_mindmap_thought_association = db.Table('user_mindmap_thought_association',
db.Column('user_mindmap_id', db.Integer, db.ForeignKey('user_mindmap.id'), primary_key=True),
db.Column('thought_id', db.Integer, db.ForeignKey('thought.id'), primary_key=True)
)
# Beziehungstabelle für Benutzer-Bookmarks von Gedanken
user_thought_bookmark = db.Table('user_thought_bookmark',
db.Column('user_id', db.Integer, db.ForeignKey('user.id'), primary_key=True),
db.Column('thought_id', db.Integer, db.ForeignKey('thought.id'), primary_key=True),
db.Column('created_at', db.DateTime, default=datetime.utcnow)
)
# Beziehungstabelle für Benutzer-Freundschaften
user_friendships = db.Table('user_friendships',
db.Column('user_id', db.Integer, db.ForeignKey('user.id'), primary_key=True),
db.Column('friend_id', db.Integer, db.ForeignKey('user.id'), primary_key=True),
db.Column('created_at', db.DateTime, default=datetime.utcnow),
db.Column('status', db.String(20), default='pending') # pending, accepted, blocked
)
# Beziehungstabelle für Benutzer-Follows
user_follows = db.Table('user_follows',
db.Column('follower_id', db.Integer, db.ForeignKey('user.id'), primary_key=True),
db.Column('followed_id', db.Integer, db.ForeignKey('user.id'), primary_key=True),
db.Column('created_at', db.DateTime, default=datetime.utcnow)
)
# Beziehungstabelle für Post-Likes
post_likes = db.Table('post_likes',
db.Column('user_id', db.Integer, db.ForeignKey('user.id'), primary_key=True),
db.Column('post_id', db.Integer, db.ForeignKey('social_post.id'), primary_key=True),
db.Column('created_at', db.DateTime, default=datetime.utcnow)
)
# Beziehungstabelle für Comment-Likes
comment_likes = db.Table('comment_likes',
db.Column('user_id', db.Integer, db.ForeignKey('user.id'), primary_key=True),
db.Column('comment_id', db.Integer, db.ForeignKey('social_comment.id'), primary_key=True),
db.Column('created_at', db.DateTime, default=datetime.utcnow)
)
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
password = db.Column(db.String(512), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
is_active = db.Column(db.Boolean, default=True)
role = db.Column(db.String(20), default="user") # 'user', 'admin', 'moderator'
bio = db.Column(db.Text, nullable=True) # Profil-Bio
location = db.Column(db.String(100), nullable=True) # Standort
website = db.Column(db.String(200), nullable=True) # Website
avatar = db.Column(db.String(200), nullable=True) # Profilbild-URL
last_login = db.Column(db.DateTime, nullable=True) # Letzter Login
# Social Network Felder
display_name = db.Column(db.String(100), nullable=True) # Anzeigename
birth_date = db.Column(db.Date, nullable=True) # Geburtsdatum
gender = db.Column(db.String(20), nullable=True) # Geschlecht
phone = db.Column(db.String(20), nullable=True) # Telefonnummer
is_verified = db.Column(db.Boolean, default=False) # Verifizierter Account
is_private = db.Column(db.Boolean, default=False) # Privater Account
follower_count = db.Column(db.Integer, default=0) # Follower-Anzahl
following_count = db.Column(db.Integer, default=0) # Following-Anzahl
post_count = db.Column(db.Integer, default=0) # Post-Anzahl
online_status = db.Column(db.String(20), default='offline') # online, offline, away
last_seen = db.Column(db.DateTime, nullable=True) # Zuletzt gesehen
# Beziehungen
threads = db.relationship('Thread', backref='creator', lazy=True)
messages = db.relationship('Message', backref='author', lazy=True)
projects = db.relationship('Project', backref='owner', lazy=True)
mindmaps = db.relationship('UserMindmap', backref='user', lazy=True)
thoughts = db.relationship('Thought', backref='author', lazy=True)
bookmarked_thoughts = db.relationship('Thought', secondary=user_thought_bookmark,
lazy='dynamic', backref=db.backref('bookmarked_by', lazy='dynamic'))
# Social Network Beziehungen
posts = db.relationship('SocialPost', backref='author', lazy=True, cascade="all, delete-orphan")
comments = db.relationship('SocialComment', backref='author', lazy=True, cascade="all, delete-orphan")
notifications = db.relationship('Notification', foreign_keys='Notification.user_id', backref='user', lazy=True, cascade="all, delete-orphan")
# Freundschaften (bidirektional)
friends = db.relationship(
'User',
secondary=user_friendships,
primaryjoin=id == user_friendships.c.user_id,
secondaryjoin=id == user_friendships.c.friend_id,
backref='friend_of',
lazy='dynamic'
)
# Following/Followers
following = db.relationship(
'User',
secondary=user_follows,
primaryjoin=id == user_follows.c.follower_id,
secondaryjoin=id == user_follows.c.followed_id,
backref=db.backref('followers', lazy='dynamic'),
lazy='dynamic'
)
# Liked Posts und Comments
liked_posts = db.relationship('SocialPost', secondary=post_likes,
backref=db.backref('liked_by', lazy='dynamic'), lazy='dynamic')
liked_comments = db.relationship('SocialComment', secondary=comment_likes,
backref=db.backref('liked_by', lazy='dynamic'), lazy='dynamic')
def __repr__(self):
return f'<User {self.username}>'
def set_password(self, password):
self.password = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password, password)
@property
def is_admin(self):
return self.role == 'admin'
@is_admin.setter
def is_admin(self, value):
self.role = 'admin' if value else 'user'
# Social Network Methoden
def follow(self, user):
"""Folgt einem anderen Benutzer"""
if not self.is_following(user):
self.following.append(user)
user.follower_count += 1
user.following_count += 1
# Notification erstellen
notification = Notification(
user_id=user.id,
type='follow',
message=f'{self.username} folgt dir jetzt',
related_user_id=self.id
)
db.session.add(notification)
def unfollow(self, user):
"""Entfolgt einem Benutzer"""
if self.is_following(user):
self.following.remove(user)
user.follower_count -= 1
user.following_count -= 1
def is_following(self, user):
"""Prüft ob der Benutzer einem anderen folgt"""
return self.following.filter(user_follows.c.followed_id == user.id).count() > 0
def get_feed_posts(self, limit=20):
"""Holt Posts für den Feed (von gefolgten Benutzern)"""
# Hole alle User-IDs von Benutzern, denen ich folge + meine eigene
followed_user_ids = [user.id for user in self.following]
all_user_ids = followed_user_ids + [self.id]
# Hole Posts von diesen Benutzern
return SocialPost.query.filter(
SocialPost.user_id.in_(all_user_ids)
).order_by(SocialPost.created_at.desc()).limit(limit)
class Category(db.Model):
"""Wissenschaftliche Kategorien für die Gliederung der öffentlichen Mindmap"""
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), unique=True, nullable=False)
description = db.Column(db.Text)
color_code = db.Column(db.String(7)) # Hex color
icon = db.Column(db.String(50))
parent_id = db.Column(db.Integer, db.ForeignKey('category.id'), nullable=True)
# Beziehungen
children = db.relationship('Category', backref=db.backref('parent', remote_side=[id]))
nodes = db.relationship('MindMapNode', backref='category', lazy=True)
def __repr__(self):
return f'<Category {self.name}>'
class MindMapNode(db.Model):
"""Öffentliche Mindmap-Knoten, die für alle Benutzer sichtbar sind"""
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
description = db.Column(db.Text)
color_code = db.Column(db.String(7))
icon = db.Column(db.String(50))
is_public = db.Column(db.Boolean, default=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
created_by_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True)
category_id = db.Column(db.Integer, db.ForeignKey('category.id'), nullable=True)
__table_args__ = {'extend_existing': True}
# Beziehungen für Baumstruktur (mehrere Eltern möglich)
parents = db.relationship(
'MindMapNode',
secondary=node_relationship,
primaryjoin=(node_relationship.c.child_id == id),
secondaryjoin=(node_relationship.c.parent_id == id),
backref=db.backref('children', lazy='dynamic'),
lazy='dynamic'
)
# Beziehungen zu Gedanken
thoughts = db.relationship('Thought',
secondary=node_thought_association,
backref=db.backref('nodes', lazy='dynamic'))
# Beziehung zum Ersteller
created_by = db.relationship('User', backref='created_nodes')
def __repr__(self):
return f'<MindMapNode {self.name}>'
def to_dict(self):
return {
'id': self.id,
'name': self.name,
'description': self.description,
'color_code': self.color_code,
'icon': self.icon,
'category_id': self.category_id,
'created_at': self.created_at.isoformat() if self.created_at else None
}
class UserMindmap(db.Model):
"""Benutzerspezifische Mindmap, die vom Benutzer personalisierbar ist"""
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
description = db.Column(db.Text)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
last_modified = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
is_private = db.Column(db.Boolean, default=True)
# Beziehungen zu öffentlichen Knoten
public_nodes = db.relationship('MindMapNode',
secondary='user_mindmap_node',
backref=db.backref('in_user_mindmaps', lazy='dynamic'))
# Beziehungen zu Gedanken
thoughts = db.relationship('Thought',
secondary=user_mindmap_thought_association,
backref=db.backref('in_user_mindmaps', lazy='dynamic'))
# Notizen zu dieser Mindmap
notes = db.relationship('MindmapNote', backref='mindmap', lazy=True)
# Beziehungstabelle für benutzerorientierte Mindmaps und öffentliche Knoten
class UserMindmapNode(db.Model):
"""Speichert die Beziehung zwischen Benutzer-Mindmaps und öffentlichen Knoten inkl. Position"""
user_mindmap_id = db.Column(db.Integer, db.ForeignKey('user_mindmap.id'), primary_key=True)
node_id = db.Column(db.Integer, db.ForeignKey('mind_map_node.id'), primary_key=True)
x_position = db.Column(db.Float, default=0) # Position X auf der Mindmap
y_position = db.Column(db.Float, default=0) # Position Y auf der Mindmap
scale = db.Column(db.Float, default=1.0) # Größe des Knotens
added_at = db.Column(db.DateTime, default=datetime.utcnow)
class MindmapNote(db.Model):
"""Private Notizen der Benutzer zu ihrer Mindmap"""
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
mindmap_id = db.Column(db.Integer, db.ForeignKey('user_mindmap.id'), nullable=False)
node_id = db.Column(db.Integer, db.ForeignKey('mind_map_node.id'), nullable=True)
thought_id = db.Column(db.Integer, db.ForeignKey('thought.id'), nullable=True)
content = db.Column(db.Text, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
last_modified = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
color_code = db.Column(db.String(7), default="#FFF59D") # Farbe der Notiz
class Thought(db.Model):
"""Gedanken und Inhalte, die in der Mindmap verknüpft werden können"""
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200), nullable=False)
content = db.Column(db.Text, nullable=False)
abstract = db.Column(db.Text)
keywords = db.Column(db.String(500))
color_code = db.Column(db.String(7)) # Hex color code
source_type = db.Column(db.String(50)) # PDF, Markdown, Text etc.
branch = db.Column(db.String(100), nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
last_modified = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Beziehungen
comments = db.relationship('Comment', backref='thought', lazy=True, cascade="all, delete-orphan")
ratings = db.relationship('ThoughtRating', backref='thought', lazy=True)
outgoing_relations = db.relationship(
'ThoughtRelation',
foreign_keys='ThoughtRelation.source_id',
backref='source_thought',
lazy=True
)
incoming_relations = db.relationship(
'ThoughtRelation',
foreign_keys='ThoughtRelation.target_id',
backref='target_thought',
lazy=True
)
@property
def average_rating(self):
if not self.ratings:
return 0
return sum(r.relevance_score for r in self.ratings) / len(self.ratings)
class ThoughtRelation(db.Model):
"""Beziehungen zwischen Gedanken"""
id = db.Column(db.Integer, primary_key=True)
source_id = db.Column(db.Integer, db.ForeignKey('thought.id'), nullable=False)
target_id = db.Column(db.Integer, db.ForeignKey('thought.id'), nullable=False)
relation_type = db.Column(db.Enum(RelationType), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
created_by_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
# Beziehung zum Ersteller
created_by = db.relationship('User', backref='created_relations')
class ThoughtRating(db.Model):
"""Bewertungen von Gedanken durch Benutzer"""
id = db.Column(db.Integer, primary_key=True)
thought_id = db.Column(db.Integer, db.ForeignKey('thought.id'), nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
relevance_score = db.Column(db.Integer, nullable=False) # 1-5
created_at = db.Column(db.DateTime, default=datetime.utcnow)
__table_args__ = (
db.UniqueConstraint('thought_id', 'user_id', name='unique_thought_rating'),
)
# Beziehung zum Benutzer
user = db.relationship('User', backref='ratings')
class Comment(db.Model):
"""Kommentare zu Gedanken"""
id = db.Column(db.Integer, primary_key=True)
content = db.Column(db.Text, nullable=False)
thought_id = db.Column(db.Integer, db.ForeignKey('thought.id'), nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
last_modified = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Thread model
class Thread(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200), nullable=False)
description = db.Column(db.Text)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
# Relationships
messages = db.relationship('Message', backref='thread', lazy=True, cascade="all, delete-orphan")
def __repr__(self):
return f'<Thread {self.title}>'
# Message model
class Message(db.Model):
id = db.Column(db.Integer, primary_key=True)
content = db.Column(db.Text, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
thread_id = db.Column(db.Integer, db.ForeignKey('thread.id'), nullable=False)
role = db.Column(db.String(20), default="user") # 'user', 'assistant', 'system'
def __repr__(self):
return f'<Message {self.id} by {self.user_id}>'
# Project model
class Project(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(150), nullable=False)
description = db.Column(db.Text)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
category_id = db.Column(db.Integer, db.ForeignKey('category.id'))
# Relationships
documents = db.relationship('Document', backref='project', lazy=True, cascade="all, delete-orphan")
def __repr__(self):
return f'<Project {self.title}>'
# Document model
class Document(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(150), nullable=False)
content = db.Column(db.Text)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
project_id = db.Column(db.Integer, db.ForeignKey('project.id'), nullable=False)
filename = db.Column(db.String(150), nullable=True)
file_path = db.Column(db.String(300), nullable=True)
file_type = db.Column(db.String(50), nullable=True)
file_size = db.Column(db.Integer, nullable=True)
def __repr__(self):
return f'<Document {self.title}>'
# Forum-Kategorie-Modell - entspricht den Hauptknotenpunkten der Mindmap
class ForumCategory(db.Model):
id = db.Column(db.Integer, primary_key=True)
node_id = db.Column(db.Integer, db.ForeignKey('mind_map_node.id'), nullable=False)
title = db.Column(db.String(200), nullable=False)
description = db.Column(db.Text)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
is_active = db.Column(db.Boolean, default=True)
# Beziehungen
node = db.relationship('MindMapNode', backref='forum_category')
posts = db.relationship('ForumPost', backref='category', lazy=True, cascade="all, delete-orphan")
def __repr__(self):
return f'<ForumCategory {self.title}>'
# Forum-Beitrag-Modell
class ForumPost(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200), nullable=False)
content = db.Column(db.Text, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
category_id = db.Column(db.Integer, db.ForeignKey('forum_category.id'), nullable=False)
parent_id = db.Column(db.Integer, db.ForeignKey('forum_post.id'), nullable=True)
is_pinned = db.Column(db.Boolean, default=False)
is_locked = db.Column(db.Boolean, default=False)
view_count = db.Column(db.Integer, default=0)
# Beziehungen
author = db.relationship('User', backref='forum_posts')
replies = db.relationship('ForumPost', backref=db.backref('parent', remote_side=[id]), lazy=True)
def __repr__(self):
return f'<ForumPost {self.title}>'
# Berechtigungstypen für Mindmap-Freigaben
class PermissionType(Enum):
READ = "Nur-Lesen"
EDIT = "Bearbeiten"
ADMIN = "Administrator"
# Freigabemodell für Mindmaps
class MindmapShare(db.Model):
"""Speichert Informationen über freigegebene Mindmaps und Berechtigungen"""
id = db.Column(db.Integer, primary_key=True)
mindmap_id = db.Column(db.Integer, db.ForeignKey('user_mindmap.id'), nullable=False)
shared_by_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
shared_with_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
permission_type = db.Column(db.Enum(PermissionType), nullable=False, default=PermissionType.READ)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
last_accessed = db.Column(db.DateTime, nullable=True)
# Beziehungen
mindmap = db.relationship('UserMindmap', backref=db.backref('shares', lazy='dynamic'))
shared_by = db.relationship('User', foreign_keys=[shared_by_id], backref=db.backref('shared_mindmaps', lazy='dynamic'))
shared_with = db.relationship('User', foreign_keys=[shared_with_id], backref=db.backref('accessible_mindmaps', lazy='dynamic'))
__table_args__ = (
db.UniqueConstraint('mindmap_id', 'shared_with_id', name='unique_mindmap_share'),
)
def __repr__(self):
return f'<MindmapShare: {self.mindmap_id} - {self.shared_with_id} - {self.permission_type.name}>'
class SocialPost(db.Model):
"""Posts im Social Network"""
id = db.Column(db.Integer, primary_key=True)
content = db.Column(db.Text, nullable=False)
image_url = db.Column(db.String(500), nullable=True) # Bild-URL
video_url = db.Column(db.String(500), nullable=True) # Video-URL
link_url = db.Column(db.String(500), nullable=True) # Link-URL
link_title = db.Column(db.String(200), nullable=True) # Link-Titel
link_description = db.Column(db.Text, nullable=True) # Link-Beschreibung
post_type = db.Column(db.String(20), default='text') # text, image, video, link, thought_share
visibility = db.Column(db.String(20), default='public') # public, friends, private
is_pinned = db.Column(db.Boolean, default=False)
like_count = db.Column(db.Integer, default=0)
comment_count = db.Column(db.Integer, default=0)
share_count = db.Column(db.Integer, default=0)
view_count = db.Column(db.Integer, default=0)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
# Verknüpfung zu Gedanken (falls der Post einen Gedanken teilt)
shared_thought_id = db.Column(db.Integer, db.ForeignKey('thought.id'), nullable=True)
shared_thought = db.relationship('Thought', backref='shared_in_posts')
# Verknüpfung zu Mindmap-Knoten
shared_node_id = db.Column(db.Integer, db.ForeignKey('mind_map_node.id'), nullable=True)
shared_node = db.relationship('MindMapNode', backref='shared_in_posts')
# Kommentare zu diesem Post
comments = db.relationship('SocialComment', backref='post', lazy=True, cascade="all, delete-orphan")
def __repr__(self):
return f'<SocialPost {self.id} by {self.author.username}>'
def to_dict(self):
return {
'id': self.id,
'content': self.content,
'post_type': self.post_type,
'image_url': self.image_url,
'video_url': self.video_url,
'link_url': self.link_url,
'link_title': self.link_title,
'link_description': self.link_description,
'visibility': self.visibility,
'is_pinned': self.is_pinned,
'like_count': self.like_count,
'comment_count': self.comment_count,
'share_count': self.share_count,
'view_count': self.view_count,
'created_at': self.created_at.isoformat(),
'updated_at': self.updated_at.isoformat(),
'author': {
'id': self.author.id,
'username': self.author.username,
'display_name': self.author.display_name or self.author.username,
'avatar': self.author.avatar,
'is_verified': self.author.is_verified
},
'shared_thought': self.shared_thought.to_dict() if self.shared_thought else None,
'shared_node': self.shared_node.to_dict() if self.shared_node else None
}
class SocialComment(db.Model):
"""Kommentare zu Posts"""
id = db.Column(db.Integer, primary_key=True)
content = db.Column(db.Text, nullable=False)
like_count = db.Column(db.Integer, default=0)
reply_count = db.Column(db.Integer, default=0)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
post_id = db.Column(db.Integer, db.ForeignKey('social_post.id'), nullable=False)
parent_id = db.Column(db.Integer, db.ForeignKey('social_comment.id'), nullable=True)
# Antworten auf diesen Kommentar
replies = db.relationship('SocialComment', backref=db.backref('parent', remote_side=[id]), lazy=True)
def __repr__(self):
return f'<SocialComment {self.id} by {self.author.username}>'
def to_dict(self):
return {
'id': self.id,
'content': self.content,
'like_count': self.like_count,
'reply_count': self.reply_count,
'created_at': self.created_at.isoformat(),
'updated_at': self.updated_at.isoformat(),
'author': {
'id': self.author.id,
'username': self.author.username,
'display_name': self.author.display_name or self.author.username,
'avatar': self.author.avatar,
'is_verified': self.author.is_verified
},
'parent_id': self.parent_id
}
class Notification(db.Model):
"""Benachrichtigungen für Benutzer"""
id = db.Column(db.Integer, primary_key=True)
type = db.Column(db.String(50), nullable=False) # follow, like, comment, mention, friend_request, etc.
message = db.Column(db.String(500), nullable=False)
is_read = db.Column(db.Boolean, default=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
# Verknüpfungen zu anderen Entitäten
related_user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True)
related_post_id = db.Column(db.Integer, db.ForeignKey('social_post.id'), nullable=True)
related_comment_id = db.Column(db.Integer, db.ForeignKey('social_comment.id'), nullable=True)
related_thought_id = db.Column(db.Integer, db.ForeignKey('thought.id'), nullable=True)
# Beziehungen
related_user = db.relationship('User', foreign_keys=[related_user_id])
related_post = db.relationship('SocialPost', foreign_keys=[related_post_id])
related_comment = db.relationship('SocialComment', foreign_keys=[related_comment_id])
related_thought = db.relationship('Thought', foreign_keys=[related_thought_id])
def __repr__(self):
return f'<Notification {self.id} for {self.user.username}>'
def to_dict(self):
return {
'id': self.id,
'type': self.type,
'message': self.message,
'is_read': self.is_read,
'created_at': self.created_at.isoformat(),
'related_user': self.related_user.username if self.related_user else None,
'related_post_id': self.related_post_id,
'related_comment_id': self.related_comment_id,
'related_thought_id': self.related_thought_id
}
class UserSettings(db.Model):
"""Benutzereinstellungen"""
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False, unique=True)
# Datenschutz-Einstellungen
profile_visibility = db.Column(db.String(20), default='public') # public, friends, private
show_email = db.Column(db.Boolean, default=False)
show_birth_date = db.Column(db.Boolean, default=False)
show_location = db.Column(db.Boolean, default=True)
allow_friend_requests = db.Column(db.Boolean, default=True)
allow_messages = db.Column(db.String(20), default='everyone') # everyone, friends, none
# Benachrichtigungs-Einstellungen
email_notifications = db.Column(db.Boolean, default=True)
push_notifications = db.Column(db.Boolean, default=True)
notify_on_follow = db.Column(db.Boolean, default=True)
notify_on_like = db.Column(db.Boolean, default=True)
notify_on_comment = db.Column(db.Boolean, default=True)
notify_on_mention = db.Column(db.Boolean, default=True)
notify_on_friend_request = db.Column(db.Boolean, default=True)
# Interface-Einstellungen
dark_mode = db.Column(db.Boolean, default=False)
language = db.Column(db.String(10), default='de')
# Beziehung
user = db.relationship('User', backref=db.backref('settings', uselist=False))
def __repr__(self):
return f'<UserSettings for {self.user.username}>'
class Activity(db.Model):
"""Aktivitätsprotokoll für Benutzer"""
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
action = db.Column(db.String(100), nullable=False) # login, logout, post_created, thought_shared, etc.
description = db.Column(db.String(500), nullable=True)
ip_address = db.Column(db.String(45), nullable=True) # IPv4/IPv6
user_agent = db.Column(db.String(500), nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# Verknüpfungen zu anderen Entitäten
related_post_id = db.Column(db.Integer, db.ForeignKey('social_post.id'), nullable=True)
related_thought_id = db.Column(db.Integer, db.ForeignKey('thought.id'), nullable=True)
related_mindmap_id = db.Column(db.Integer, db.ForeignKey('user_mindmap.id'), nullable=True)
# Beziehungen
user = db.relationship('User', backref='activities')
related_post = db.relationship('SocialPost')
related_thought = db.relationship('Thought')
related_mindmap = db.relationship('UserMindmap')
def __repr__(self):
return f'<Activity {self.action} by {self.user.username}>'

View File

@@ -1,6 +1,17 @@
flask
flask-login
flask==2.2.5
flask-login==0.6.2
flask-wtf
email-validator
python-dotenv
flask-sqlalchemy
werkzeug==2.2.3
flask-sqlalchemy==3.0.5
openai
requests==2.31.0
gunicorn==21.2.0
#pillow==10.0.1
pytest==7.4.0
pytest-flask==1.2.0
Flask-Migrate
flask-socketio==5.3.6
python-engineio==4.8.2
python-socketio==5.11.1

22
server.log Normal file
View File

@@ -0,0 +1,22 @@
⏰ 21:58:48.486 │ ✅ INFO  │ ⚙ [SYSTEM ] │ 📝 🚀 SysTades Social Network gestartet
⏰ 21:58:48.486 │ ✅ INFO  │ ⚙ [SYSTEM ] │ 📝 🚀 SysTades Social Network gestartet (v1.0.0) in development Umgebung auf Port 5000
⏰ 21:58:49.951 │ ✅ INFO  │ ⚙ [SYSTEM ] │ 📝 OpenAI API-Verbindung erfolgreich hergestellt
⏰ 21:58:50.122 │ ✅ INFO  │ 🗄 [DB ] │ 🚫 Datenbank erfolgreich initialisiert
⏰ 21:58:50.132 │ ✅ INFO  │ 🗄 [DB ] │ 🚫 Datenbanktabellen erstellt/aktualisiert
⏰ 21:58:50.134 │ ✅ INFO  │ ⚙ [SYSTEM ] │ 📝 Starte Flask-Entwicklungsserver auf http://localhost:5000
* Serving Flask app 'app'
* Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
* Restarting with watchdog (inotify)
⏰ 21:58:52.225 │ ✅ INFO  │ ⚙ [SYSTEM ] │ 📝 🚀 SysTades Social Network gestartet
⏰ 21:58:52.226 │ ✅ INFO  │ ⚙ [SYSTEM ] │ 📝 🚀 SysTades Social Network gestartet (v1.0.0) in development Umgebung auf Port 5000
⏰ 21:58:53.848 │ ✅ INFO  │ ⚙ [SYSTEM ] │ 📝 OpenAI API-Verbindung erfolgreich hergestellt
⏰ 21:58:53.997 │ ✅ INFO  │ 🗄 [DB ] │ 🚫 Datenbank erfolgreich initialisiert
⏰ 21:58:54.002 │ ✅ INFO  │ 🗄 [DB ] │ 🚫 Datenbanktabellen erstellt/aktualisiert
⏰ 21:58:54.006 │ ✅ INFO  │ ⚙ [SYSTEM ] │ 📝 Starte Flask-Entwicklungsserver auf http://localhost:5000
* Debugger is active!
* Debugger PIN: 114-005-893

53
start.sh Normal file
View File

@@ -0,0 +1,53 @@
#!/usr/bin/env powershell
# Windows PowerShell-Version des Start-Skripts
# Datum: 01.05.2025
# Docker-Status prüfen
Write-Host "Prüfe Docker-Status..." -ForegroundColor Cyan
try {
$status = docker ps -q
if ($LASTEXITCODE -ne 0) {
Write-Host "Docker ist nicht gestartet. Bitte starten Sie Docker Desktop." -ForegroundColor Red
exit 1
}
} catch {
Write-Host "Docker ist nicht verfügbar. Bitte installieren Sie Docker Desktop und starten Sie es." -ForegroundColor Red
Write-Host $_.Exception.Message
exit 1
}
# Alte Container stoppen und entfernen
$containerExists = docker ps -a --filter "name=systades_app" -q
if ($containerExists) {
Write-Host "Stoppe und entferne alten Container..." -ForegroundColor Yellow
docker rm -f systades_app
}
# Alte Images löschen
Write-Host "Entferne altes Image..." -ForegroundColor Yellow
docker rmi -f systades_app:latest
# Stelle sicher, dass das Datenbankverzeichnis existiert
if (-not (Test-Path "database")) {
New-Item -Path "database" -ItemType Directory -Force
}
# Docker-Compose Setup neu bauen
Write-Host "Baue Container neu..." -ForegroundColor Green
docker-compose build --no-cache
# Docker-Compose neu starten
Write-Host "Starte Container..." -ForegroundColor Green
docker-compose up -d --force-recreate
# Warte kurz und prüfe, ob der Container läuft
Write-Host "Prüfe Container-Status..." -ForegroundColor Cyan
Start-Sleep -Seconds 3
docker ps | Select-String "systades_app"
# Ausgabe
Write-Host "`nSystemstatus:" -ForegroundColor Cyan
Write-Host "----------------------------------------"
Write-Host "Systades-Anwendung ist jetzt unter http://localhost:5000 erreichbar." -ForegroundColor Green
Write-Host "Container-Logs können mit 'docker logs -f systades_app' angezeigt werden." -ForegroundColor Green
Write-Host "----------------------------------------"

View File

@@ -0,0 +1 @@

27
static/css/all.min.css vendored Normal file
View File

@@ -0,0 +1,27 @@
/*
* Font Awesome 6.4.0
*
* This is a placeholder file. For production, you should:
* 1. Download Font Awesome from https://fontawesome.com/download
* 2. Extract the downloaded package
* 3. Copy the 'css/all.min.css' file to this location
* 4. Copy the 'webfonts' folder to '/static/webfonts/'
*
* Alternatively, you can install via npm and copy the files:
* npm install @fortawesome/fontawesome-free
* cp -r node_modules/@fortawesome/fontawesome-free/css/all.min.css static/css/
* cp -r node_modules/@fortawesome/fontawesome-free/webfonts/ static/
*/
/* Placeholder styles for common Font Awesome icons */
.fa, .fas, .far, .fab {
display: inline-block;
width: 1em;
text-align: center;
}
/* Warning message */
body::before {
content: "Font Awesome CSS placeholder. Please replace with the actual file.";
display: none;
}

252
static/css/assistant.css Normal file
View File

@@ -0,0 +1,252 @@
/* ChatGPT Assistent Styles - Verbesserte Version */
#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);
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;
}
#assistant-toggle:hover {
transform: scale(1.1) rotate(10deg);
}
#assistant-history::-webkit-scrollbar {
width: 6px;
}
#assistant-history::-webkit-scrollbar-track {
background: transparent;
}
#assistant-history::-webkit-scrollbar-thumb {
background-color: rgba(156, 163, 175, 0.5);
border-radius: 3px;
}
.dark #assistant-history::-webkit-scrollbar-thumb {
background-color: rgba(156, 163, 175, 0.3);
}
/* Verbesserte Message-Bubbles mit Schatten und Animation */
#assistant-history .flex > div {
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
animation: messageAppear 0.3s ease-out forwards;
opacity: 0;
transform: translateY(10px);
}
@keyframes messageAppear {
to {
opacity: 1;
transform: translateY(0);
}
}
/* Verzögerte Animation für Messages */
#assistant-history .flex:nth-child(1) > div { animation-delay: 0.05s; }
#assistant-history .flex:nth-child(2) > div { animation-delay: 0.1s; }
#assistant-history .flex:nth-child(3) > div { animation-delay: 0.15s; }
#assistant-history .flex:nth-child(4) > div { animation-delay: 0.2s; }
#assistant-history .flex:nth-child(5) > div { animation-delay: 0.25s; }
/* Vorschläge styling */
#assistant-suggestions {
padding: 0.5rem 0.75rem;
transition: all 0.3s ease;
}
.suggestion-pill {
animation: pillAppear 0.4s ease forwards;
opacity: 0;
transform: scale(0.9);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
@keyframes pillAppear {
to {
opacity: 1;
transform: scale(1);
}
}
/* Styling für verschiedene Verzögerungen bei Vorschlägen */
#assistant-suggestions button:nth-child(1) { animation-delay: 0.1s; }
#assistant-suggestions button:nth-child(2) { animation-delay: 0.2s; }
#assistant-suggestions button:nth-child(3) { animation-delay: 0.3s; }
/* Mach Platz für Notifications, damit sie nicht mit dem Assistenten überlappen */
.notification-area {
bottom: 5rem;
}
/* Verbesserte Glassmorphism-Effekt */
.glass-morphism {
background: rgba(255, 255, 255, 0.25);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.15);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.dark .glass-morphism {
background: rgba(15, 23, 42, 0.35);
border: 1px solid rgba(255, 255, 255, 0.06);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.4);
}
/* Verbesserte Farbpalette für Dark Theme */
.dark {
--tw-bg-opacity: 1;
background-color: rgba(10, 15, 25, var(--tw-bg-opacity)) !important;
min-height: 100vh;
}
.dark .bg-dark-900 {
--tw-bg-opacity: 1;
background-color: rgba(10, 15, 25, var(--tw-bg-opacity)) !important;
}
.dark .bg-dark-800 {
--tw-bg-opacity: 1;
background-color: rgba(15, 23, 42, var(--tw-bg-opacity)) !important;
}
.dark .bg-dark-700 {
--tw-bg-opacity: 1;
background-color: rgba(23, 33, 64, var(--tw-bg-opacity)) !important;
}
/* Typing Indicator Animation Styles */
.typing-indicator {
display: flex;
align-items: center;
}
.typing-indicator span {
height: 8px;
width: 8px;
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%;
margin: 0;
min-height: 100vh;
}
body {
display: flex;
flex-direction: column;
min-height: 100vh;
}
main {
flex: 1 0 auto;
}
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;
}

1068
static/css/base-styles.css Normal file

File diff suppressed because it is too large Load Diff

3884
static/css/main.css Normal file

File diff suppressed because it is too large Load Diff

437
static/css/mindmap.css Normal file
View File

@@ -0,0 +1,437 @@
/* Mindmap Container Styles */
.mindmap-container {
position: relative;
width: 100%;
height: 100%;
min-height: 600px;
background: var(--bg-primary);
border-radius: 12px;
overflow: hidden;
}
/* Cytoscape Container für die Hauptmindmap */
#cy {
width: 100%;
height: 100%;
background: transparent;
position: absolute;
top: 0;
left: 0;
z-index: 1;
}
/* Subpage Styles - Identisches Design wie Hauptmindmap */
.mindmap-subpage {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--bg-primary);
display: flex;
flex-direction: column;
z-index: 10;
transition: opacity 0.3s ease, transform 0.3s ease;
opacity: 1;
transform: translateY(0);
}
/* Subpage Header */
.subpage-header {
display: flex;
align-items: center;
padding: 16px;
background: var(--bg-secondary);
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
z-index: 2;
}
.dark .subpage-header {
background: rgba(30, 41, 59, 0.8);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
/* Zurück-Button */
.back-button {
background: rgba(255, 255, 255, 0.1);
border: none;
color: white;
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
margin-right: 12px;
}
.back-button:hover {
background: rgba(255, 255, 255, 0.2);
transform: translateY(-2px);
}
/* Subpage Titel */
.subpage-title {
font-size: 1.5rem;
font-weight: 600;
color: white;
margin: 0;
background: linear-gradient(90deg, #60a5fa, #8b5cf6);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
/* Subpage Cytoscape Container */
.subpage-cy-container {
position: relative;
flex: 1;
width: 100%;
height: calc(100% - 72px);
overflow: hidden;
z-index: 1;
}
/* Toolbar für Zoom-Kontrollen */
.mindmap-toolbar {
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 8px;
padding: 8px;
background: rgba(30, 41, 59, 0.8);
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
z-index: 20;
backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.mindmap-toolbar button {
width: 40px;
height: 40px;
border: none;
background: rgba(255, 255, 255, 0.1);
color: white;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
}
.mindmap-toolbar button:hover {
background: rgba(139, 92, 246, 0.5);
transform: translateY(-2px);
}
.mindmap-toolbar button i {
font-size: 16px;
}
/* Mindmap Header */
.mindmap-header {
position: absolute;
top: 0;
left: 0;
right: 0;
padding: 1.5rem;
background: rgba(15, 23, 42, 0.8);
backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
z-index: 10;
display: flex;
justify-content: space-between;
align-items: center;
}
/* Dark Mode spezifische Stile */
.dark .mindmap-subpage {
background: linear-gradient(135deg, #0f172a 0%, #0c1221 100%);
}
/* Fix für Zoom-Buttons */
body.dark .mindmap-toolbar button {
background: rgba(255, 255, 255, 0.1);
color: white;
}
body:not(.dark) .mindmap-toolbar button {
background: rgba(30, 41, 59, 0.2);
color: #1e293b;
}
/* Kontext-Menü-Anpassungen */
.context-menu {
z-index: 1000;
}
/* Export Group Styles */
.export-group {
position: relative;
}
.export-options {
position: absolute;
top: 100%;
right: 0;
margin-top: 8px;
padding: 8px;
background: var(--bg-secondary);
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
display: none;
flex-direction: column;
gap: 4px;
min-width: 160px;
}
.dark .export-options {
background: rgba(30, 41, 59, 0.9);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.export-group:hover .export-options {
display: flex;
}
.export-options button {
width: 100%;
height: auto;
padding: 8px 12px;
justify-content: flex-start;
font-size: 14px;
border-radius: 6px;
}
/* Context Menu Styles */
.mindmap-context-menu {
position: fixed;
background: var(--bg-secondary);
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
padding: 8px;
z-index: 1000;
min-width: 180px;
}
.dark .mindmap-context-menu {
background: rgba(30, 41, 59, 0.9);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.mindmap-context-menu button {
display: flex;
align-items: center;
width: 100%;
padding: 8px 12px;
border: none;
background: transparent;
color: var(--text-primary);
cursor: pointer;
font-size: 14px;
border-radius: 6px;
transition: all 0.2s ease;
}
.mindmap-context-menu button:hover {
background: var(--accent-primary);
color: white;
}
.mindmap-context-menu button i {
margin-right: 8px;
width: 16px;
}
/* Node Styles */
.mindmap-node {
background-color: var(--bg-secondary);
border: 2px solid var(--accent-primary);
border-radius: 8px;
padding: 8px 12px;
transition: all 0.3s ease;
}
.mindmap-node:hover {
box-shadow: 0 0 0 2px var(--accent-primary);
transform: scale(1.05);
}
.mindmap-node.selected {
border-color: var(--accent-secondary);
box-shadow: 0 0 0 3px var(--accent-secondary);
}
/* Edge Styles */
.mindmap-edge {
width: 2px;
transition: all 0.3s ease;
}
.dark .mindmap-edge {
background-color: rgba(255, 255, 255, 0.2);
}
.mindmap-edge:hover {
width: 3px;
background-color: var(--accent-primary);
}
/* Animation Styles */
@keyframes nodeAppear {
from {
opacity: 0;
transform: scale(0.8);
}
to {
opacity: 1;
transform: scale(1);
}
}
.mindmap-node-new {
animation: nodeAppear 0.3s ease forwards;
}
/* Responsive Styles */
@media (max-width: 768px) {
.mindmap-toolbar {
flex-wrap: wrap;
width: calc(100% - 32px);
justify-content: center;
}
.export-options {
left: 0;
right: auto;
}
}
/* Loading State */
.mindmap-loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.mindmap-loading-spinner {
width: 40px;
height: 40px;
border: 4px solid var(--bg-secondary);
border-top-color: var(--accent-primary);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Tooltip Styles */
.mindmap-tooltip {
position: absolute;
background: var(--bg-secondary);
padding: 8px 12px;
border-radius: 6px;
font-size: 12px;
pointer-events: none;
z-index: 1000;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
max-width: 200px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.dark .mindmap-tooltip {
background: rgba(30, 41, 59, 0.9);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
/* Kategorien-Panel */
.categories-panel {
position: absolute;
top: 80px;
left: 20px;
width: 300px;
max-height: calc(100vh - 120px);
background: rgba(15, 23, 42, 0.95);
border-radius: 12px;
padding: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
z-index: 1000;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
transform: translateX(-320px);
transition: transform 0.3s ease-in-out;
overflow-y: auto;
}
.categories-panel.visible {
transform: translateX(0);
}
.categories-panel h3 {
color: white;
font-size: 1.2rem;
margin: 0 0 16px 0;
padding-bottom: 8px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.category-list {
list-style: none;
padding: 0;
margin: 0;
}
.category-item {
display: flex;
align-items: center;
padding: 8px 12px;
margin: 4px 0;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
color: white;
}
.category-item:hover {
background: rgba(255, 255, 255, 0.1);
transform: translateX(4px);
}
.category-color {
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 12px;
flex-shrink: 0;
}
.category-name {
flex-grow: 1;
font-size: 0.95rem;
}
.category-count {
background: rgba(255, 255, 255, 0.1);
padding: 2px 8px;
border-radius: 12px;
font-size: 0.8rem;
margin-left: 8px;
}

View File

@@ -0,0 +1,106 @@
/* Neural Network Background CSS */
/* Make sure the neural network background is always visible */
#neural-network-background {
position: fixed !important;
top: 0 !important;
left: 0 !important;
width: 100% !important;
height: 100% !important;
z-index: -10 !important; /* Below content but above regular background */
pointer-events: none !important;
opacity: 1 !important;
}
/* Override any solid background colors for the body */
body, body.dark {
background-color: transparent !important;
}
/* Make sure any background color is removed */
html.dark, html {
background-color: transparent !important;
}
/* Make sure any fixed backgrounds are removed */
#app-container {
background-color: transparent !important;
}
/* Ensure content is properly visible over the background */
.glass-morphism {
background-color: rgba(17, 24, 39, 0.6) !important;
backdrop-filter: blur(5px) !important;
}
/* Dark Mode - Navbar */
body.dark .glass-navbar-dark {
background-color: rgba(10, 14, 25, 0.7) !important;
}
/* Light Mode - Verbesserter Navbar */
body .glass-navbar-light {
background-color: rgba(255, 255, 255, 0.92) !important;
backdrop-filter: blur(10px) !important;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06) !important;
border-bottom: 1px solid rgba(220, 220, 220, 0.5) !important;
}
/* Light Mode - Verbesserte Lesbarkeit für Navbar-Elemente */
body:not(.dark) .navbar-link,
body:not(.dark) .navbar-item {
color: #1e3a8a !important; /* Dunkles Blau für bessere Lesbarkeit */
}
body:not(.dark) .navbar-link:hover,
body:not(.dark) .navbar-item:hover {
color: #4f46e5 !important; /* Helles Lila beim Hover */
background-color: rgba(240, 245, 255, 0.9) !important;
}
/* Light Mode - Buttons verbessert */
body:not(.dark) .btn,
body:not(.dark) button {
background-color: #3b82f6 !important; /* Klares Blau statt Grau */
color: white !important;
border: none !important;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important;
}
body:not(.dark) .btn:hover,
body:not(.dark) button:hover {
background-color: #4f46e5 !important; /* Lila beim Hover */
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.12) !important;
}
/* Verbesserte Karten im Light Mode */
body:not(.dark) .card,
body:not(.dark) .panel {
background-color: rgba(255, 255, 255, 0.92) !important;
border: 1px solid rgba(220, 220, 220, 0.8) !important;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05) !important;
}
/* Verbesserte Lesbarkeit für Text im Light Mode */
body:not(.dark) {
color: #1e293b !important; /* Dunkles Blau-Grau statt Schwarz */
}
body:not(.dark) h1,
body:not(.dark) h2,
body:not(.dark) h3,
body:not(.dark) h4,
body:not(.dark) h5,
body:not(.dark) h6 {
color: #0f172a !important; /* Fast schwarz für Überschriften */
}
/* Make sure footer has proper transparency and styling */
body.dark footer {
background-color: rgba(10, 14, 25, 0.7) !important;
}
body:not(.dark) footer {
background-color: rgba(249, 250, 251, 0.92) !important;
border-top: 1px solid rgba(220, 220, 220, 0.8) !important;
}

915
static/css/social.css Normal file
View File

@@ -0,0 +1,915 @@
/* ================================
SysTades Social Network Styles
================================ */
:root {
/* Primary Colors */
--primary-50: #f0f9ff;
--primary-100: #e0f2fe;
--primary-200: #bae6fd;
--primary-300: #7dd3fc;
--primary-400: #38bdf8;
--primary-500: #0ea5e9;
--primary-600: #0284c7;
--primary-700: #0369a1;
--primary-800: #075985;
--primary-900: #0c4a6e;
/* Neutral Colors */
--gray-50: #f9fafb;
--gray-100: #f3f4f6;
--gray-200: #e5e7eb;
--gray-300: #d1d5db;
--gray-400: #9ca3af;
--gray-500: #6b7280;
--gray-600: #4b5563;
--gray-700: #374151;
--gray-800: #1f2937;
--gray-900: #111827;
/* Semantic Colors */
--success: #10b981;
--warning: #f59e0b;
--error: #ef4444;
--info: #3b82f6;
/* Social Media Colors */
--like-color: #ec4899;
--share-color: #8b5cf6;
--bookmark-color: #f59e0b;
--comment-color: var(--primary-500);
/* Glassmorphism */
--glass-bg: rgba(255, 255, 255, 0.1);
--glass-border: rgba(255, 255, 255, 0.2);
--glass-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
/* Animations */
--transition-fast: 0.15s ease-out;
--transition-normal: 0.3s ease-out;
--transition-slow: 0.6s ease-out;
/* Shadows */
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
/* Border Radius */
--radius-sm: 0.375rem;
--radius-md: 0.5rem;
--radius-lg: 0.75rem;
--radius-xl: 1rem;
--radius-2xl: 1.5rem;
}
/* Dark Mode Variables */
[data-theme="dark"] {
--glass-bg: rgba(0, 0, 0, 0.1);
--glass-border: rgba(255, 255, 255, 0.1);
--glass-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
}
/* ================================
Performance Optimizations
================================ */
* {
box-sizing: border-box;
}
img {
max-width: 100%;
height: auto;
}
/* GPU Acceleration for animations */
.accelerated {
transform: translateZ(0);
will-change: transform;
}
/* Smooth scrolling */
html {
scroll-behavior: smooth;
}
/* ================================
Social Feed Styles
================================ */
.social-feed {
max-width: 600px;
margin: 0 auto;
padding: 1rem;
}
.post-card {
background: var(--glass-bg);
backdrop-filter: blur(20px);
border: 1px solid var(--glass-border);
border-radius: var(--radius-xl);
margin-bottom: 1.5rem;
padding: 1.5rem;
box-shadow: var(--shadow-lg);
transition: all var(--transition-normal);
position: relative;
overflow: hidden;
}
.post-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-xl);
border-color: var(--primary-300);
}
.post-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, var(--primary-500), var(--primary-600));
opacity: 0;
transition: opacity var(--transition-normal);
}
.post-card:hover::before {
opacity: 1;
}
/* Post Header */
.post-header {
display: flex;
align-items: center;
margin-bottom: 1rem;
gap: 0.75rem;
}
.post-avatar {
width: 48px;
height: 48px;
border-radius: 50%;
border: 2px solid var(--primary-500);
transition: all var(--transition-fast);
cursor: pointer;
}
.post-avatar:hover {
transform: scale(1.1);
border-color: var(--primary-400);
}
.post-author {
flex: 1;
}
.post-author-name {
font-weight: 600;
color: var(--gray-800);
margin: 0;
font-size: 1rem;
}
.post-author-username {
color: var(--gray-500);
font-size: 0.875rem;
margin: 0;
}
.post-time {
color: var(--gray-400);
font-size: 0.875rem;
}
/* Post Content */
.post-content {
margin-bottom: 1rem;
line-height: 1.6;
color: var(--gray-700);
}
.post-type-badge {
display: inline-block;
padding: 0.25rem 0.75rem;
border-radius: var(--radius-md);
font-size: 0.75rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.5rem;
}
.post-type-text { background: var(--gray-100); color: var(--gray-600); }
.post-type-thought { background: var(--primary-100); color: var(--primary-600); }
.post-type-question { background: var(--warning); color: white; }
.post-type-insight { background: var(--success); color: white; }
/* Post Actions */
.post-actions {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 1rem;
border-top: 1px solid var(--gray-200);
}
.action-group {
display: flex;
gap: 1rem;
}
.action-btn {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border: none;
background: transparent;
border-radius: var(--radius-md);
cursor: pointer;
transition: all var(--transition-fast);
font-size: 0.875rem;
color: var(--gray-500);
}
.action-btn:hover {
background: var(--gray-100);
color: var(--gray-700);
transform: translateY(-1px);
}
.action-btn.active {
color: var(--primary-600);
background: var(--primary-50);
}
.action-btn i {
font-size: 1rem;
}
/* Specific action colors */
.action-btn.like-btn.active {
color: var(--like-color);
background: rgba(236, 72, 153, 0.1);
}
.action-btn.share-btn:hover {
color: var(--share-color);
background: rgba(139, 92, 246, 0.1);
}
.action-btn.bookmark-btn.active {
color: var(--bookmark-color);
background: rgba(245, 158, 11, 0.1);
}
/* ================================
Comments Section
================================ */
.comments-section {
border-top: 1px solid var(--gray-200);
padding-top: 1rem;
margin-top: 1rem;
}
.comment-item {
display: flex;
gap: 0.75rem;
margin-bottom: 1rem;
padding: 0.75rem;
border-radius: var(--radius-lg);
background: var(--gray-50);
transition: background var(--transition-fast);
}
.comment-item:hover {
background: var(--gray-100);
}
.comment-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
border: 2px solid var(--primary-400);
}
.comment-content {
flex: 1;
}
.comment-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.25rem;
}
.comment-author {
font-weight: 600;
color: var(--gray-800);
font-size: 0.875rem;
}
.comment-time {
color: var(--gray-400);
font-size: 0.75rem;
}
.comment-text {
color: var(--gray-700);
font-size: 0.875rem;
line-height: 1.5;
margin-bottom: 0.5rem;
}
.comment-actions {
display: flex;
gap: 1rem;
}
.comment-action {
background: none;
border: none;
color: var(--gray-400);
font-size: 0.75rem;
cursor: pointer;
transition: color var(--transition-fast);
}
.comment-action:hover {
color: var(--primary-500);
}
/* Comment Form */
.comment-form {
display: flex;
gap: 0.75rem;
margin-top: 1rem;
}
.comment-form textarea {
flex: 1;
padding: 0.75rem;
border: 1px solid var(--gray-300);
border-radius: var(--radius-lg);
resize: none;
min-height: 80px;
transition: border-color var(--transition-fast);
}
.comment-form textarea:focus {
outline: none;
border-color: var(--primary-500);
box-shadow: 0 0 0 3px rgba(14, 165, 233, 0.1);
}
.comment-submit {
padding: 0.75rem 1.5rem;
background: var(--primary-500);
color: white;
border: none;
border-radius: var(--radius-lg);
cursor: pointer;
transition: all var(--transition-fast);
font-weight: 500;
}
.comment-submit:hover {
background: var(--primary-600);
transform: translateY(-1px);
}
/* ================================
Create Post Form
================================ */
.create-post-form {
background: var(--glass-bg);
backdrop-filter: blur(20px);
border: 1px solid var(--glass-border);
border-radius: var(--radius-xl);
padding: 1.5rem;
margin-bottom: 2rem;
box-shadow: var(--shadow-md);
}
.create-post-textarea {
width: 100%;
min-height: 120px;
padding: 1rem;
border: 1px solid var(--gray-300);
border-radius: var(--radius-lg);
resize: vertical;
font-family: inherit;
font-size: 1rem;
line-height: 1.5;
transition: all var(--transition-normal);
}
.create-post-textarea:focus {
outline: none;
border-color: var(--primary-500);
box-shadow: 0 0 0 3px rgba(14, 165, 233, 0.1);
}
.create-post-options {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 1rem;
gap: 1rem;
}
.post-type-select,
.post-visibility-select {
padding: 0.5rem 1rem;
border: 1px solid var(--gray-300);
border-radius: var(--radius-md);
background: white;
cursor: pointer;
transition: border-color var(--transition-fast);
}
.post-type-select:focus,
.post-visibility-select:focus {
outline: none;
border-color: var(--primary-500);
}
.create-post-btn {
padding: 0.75rem 2rem;
background: linear-gradient(135deg, var(--primary-500), var(--primary-600));
color: white;
border: none;
border-radius: var(--radius-lg);
cursor: pointer;
font-weight: 600;
transition: all var(--transition-normal);
box-shadow: var(--shadow-md);
}
.create-post-btn:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
}
.create-post-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
/* ================================
Filter Tabs
================================ */
.feed-filters {
display: flex;
gap: 0.5rem;
margin-bottom: 2rem;
padding: 0.5rem;
background: var(--gray-100);
border-radius: var(--radius-xl);
overflow-x: auto;
}
.filter-tab {
padding: 0.75rem 1.5rem;
border: none;
background: transparent;
border-radius: var(--radius-lg);
cursor: pointer;
transition: all var(--transition-fast);
font-weight: 500;
white-space: nowrap;
color: var(--gray-600);
}
.filter-tab:hover {
background: var(--gray-200);
color: var(--gray-800);
}
.filter-tab.active {
background: var(--primary-500);
color: white;
box-shadow: var(--shadow-md);
}
/* ================================
Notifications
================================ */
.notification-item {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem;
border-radius: var(--radius-lg);
transition: all var(--transition-fast);
cursor: pointer;
position: relative;
}
.notification-item:hover {
background: var(--gray-50);
transform: translateX(4px);
}
.notification-item.unread {
background: var(--primary-50);
border-left: 4px solid var(--primary-500);
}
.notification-icon {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
color: white;
}
.notification-like { background: var(--like-color); }
.notification-comment { background: var(--comment-color); }
.notification-follow { background: var(--success); }
.notification-share { background: var(--share-color); }
.notification-content {
flex: 1;
}
.notification-text {
margin: 0 0 0.25rem 0;
color: var(--gray-800);
font-size: 0.875rem;
}
.notification-time {
color: var(--gray-500);
font-size: 0.75rem;
}
.notification-actions {
display: flex;
gap: 0.5rem;
}
.notification-delete {
background: none;
border: none;
color: var(--gray-400);
cursor: pointer;
padding: 0.5rem;
border-radius: var(--radius-md);
transition: all var(--transition-fast);
}
.notification-delete:hover {
background: var(--error);
color: white;
}
/* ================================
User Profile
================================ */
.profile-header {
background: linear-gradient(135deg, var(--primary-500), var(--primary-600));
color: white;
padding: 2rem;
border-radius: var(--radius-2xl);
margin-bottom: 2rem;
position: relative;
overflow: hidden;
}
.profile-header::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grain" width="100" height="100" patternUnits="userSpaceOnUse"><circle cx="25" cy="25" r="1" fill="rgba(255,255,255,0.1)"/><circle cx="75" cy="75" r="1" fill="rgba(255,255,255,0.1)"/></pattern></defs><rect width="100" height="100" fill="url(%23grain)"/></svg>');
opacity: 0.3;
}
.profile-info {
position: relative;
z-index: 1;
display: flex;
align-items: center;
gap: 1.5rem;
}
.profile-avatar {
width: 100px;
height: 100px;
border-radius: 50%;
border: 4px solid rgba(255, 255, 255, 0.3);
box-shadow: var(--shadow-xl);
}
.profile-details h1 {
margin: 0 0 0.5rem 0;
font-size: 2rem;
font-weight: 700;
}
.profile-username {
opacity: 0.9;
font-size: 1.1rem;
margin-bottom: 0.5rem;
}
.profile-bio {
opacity: 0.8;
line-height: 1.5;
max-width: 500px;
}
.profile-stats {
display: flex;
gap: 2rem;
margin-top: 1.5rem;
}
.stat-item {
text-align: center;
}
.stat-number {
display: block;
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 0.25rem;
}
.stat-label {
font-size: 0.875rem;
opacity: 0.8;
}
.follow-btn {
background: rgba(255, 255, 255, 0.2);
border: 2px solid rgba(255, 255, 255, 0.3);
color: white;
padding: 0.75rem 2rem;
border-radius: var(--radius-lg);
font-weight: 600;
cursor: pointer;
transition: all var(--transition-normal);
backdrop-filter: blur(10px);
}
.follow-btn:hover {
background: rgba(255, 255, 255, 0.3);
transform: translateY(-2px);
}
.follow-btn.following {
background: rgba(255, 255, 255, 0.9);
color: var(--primary-600);
}
/* Profile Tabs */
.profile-tabs {
display: flex;
border-bottom: 1px solid var(--gray-200);
margin-bottom: 2rem;
overflow-x: auto;
}
.profile-tab {
padding: 1rem 2rem;
border: none;
background: none;
cursor: pointer;
position: relative;
color: var(--gray-600);
font-weight: 500;
transition: all var(--transition-fast);
white-space: nowrap;
}
.profile-tab:hover {
color: var(--primary-600);
}
.profile-tab.active {
color: var(--primary-600);
}
.profile-tab.active::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 3px;
background: var(--primary-500);
border-radius: 2px 2px 0 0;
}
/* ================================
Responsive Design
================================ */
@media (max-width: 768px) {
.social-feed {
padding: 0.5rem;
}
.post-card {
padding: 1rem;
margin-bottom: 1rem;
}
.post-actions {
flex-direction: column;
gap: 1rem;
align-items: stretch;
}
.action-group {
justify-content: space-around;
}
.create-post-options {
flex-direction: column;
align-items: stretch;
gap: 0.75rem;
}
.feed-filters {
padding: 0.25rem;
gap: 0.25rem;
}
.filter-tab {
padding: 0.5rem 1rem;
font-size: 0.875rem;
}
.profile-header {
padding: 1.5rem;
}
.profile-info {
flex-direction: column;
text-align: center;
gap: 1rem;
}
.profile-avatar {
width: 80px;
height: 80px;
}
.profile-details h1 {
font-size: 1.5rem;
}
.profile-stats {
justify-content: center;
gap: 1.5rem;
}
.profile-tabs {
gap: 0;
}
.profile-tab {
flex: 1;
padding: 0.75rem 1rem;
text-align: center;
font-size: 0.875rem;
}
}
/* ================================
Loading & Animations
================================ */
.loading-spinner {
display: inline-block;
width: 20px;
height: 20px;
border: 2px solid var(--gray-300);
border-radius: 50%;
border-top-color: var(--primary-500);
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.fade-in {
animation: fadeIn 0.5s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.slide-up {
animation: slideUp 0.3s ease-out;
}
@keyframes slideUp {
from { transform: translateY(100%); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.pulse {
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* ================================
Toast Notifications
================================ */
.toast-container {
position: fixed;
top: 1rem;
right: 1rem;
z-index: 1000;
pointer-events: none;
}
.toast {
background: var(--glass-bg);
backdrop-filter: blur(20px);
border: 1px solid var(--glass-border);
border-radius: var(--radius-lg);
padding: 1rem 1.5rem;
margin-bottom: 0.5rem;
box-shadow: var(--shadow-lg);
pointer-events: all;
max-width: 400px;
animation: slideInRight 0.3s ease-out;
}
.toast.success {
border-left: 4px solid var(--success);
}
.toast.error {
border-left: 4px solid var(--error);
}
.toast.warning {
border-left: 4px solid var(--warning);
}
.toast.info {
border-left: 4px solid var(--info);
}
@keyframes slideInRight {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
/* ================================
Utilities
================================ */
.text-gradient {
background: linear-gradient(135deg, var(--primary-500), var(--primary-600));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.glass-effect {
background: var(--glass-bg);
backdrop-filter: blur(20px);
border: 1px solid var(--glass-border);
}
.shadow-glow {
box-shadow: 0 0 20px rgba(14, 165, 233, 0.3);
}
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}

207
static/css/src/input.css Normal file
View File

@@ -0,0 +1,207 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
html {
@apply scroll-smooth;
}
body {
@apply bg-gray-50 text-gray-800 dark:bg-dark-800 dark:text-gray-100 font-sans;
}
h1, h2, h3, h4, h5, h6 {
@apply font-bold text-gray-900 dark:text-white;
}
h1 {
@apply text-4xl md:text-5xl lg:text-6xl;
}
h2 {
@apply text-3xl md:text-4xl;
}
h3 {
@apply text-2xl md:text-3xl;
}
h4 {
@apply text-xl md:text-2xl;
}
a {
@apply text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300 transition-colors;
}
input, textarea, select {
@apply bg-white border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 dark:bg-dark-700 dark:border-dark-500 dark:focus:ring-primary-400 dark:focus:border-primary-400;
}
label {
@apply block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1;
}
::placeholder {
@apply text-gray-400 dark:text-gray-500;
}
}
@layer components {
.btn {
@apply inline-flex items-center justify-center px-4 py-2 font-medium rounded-md transition-all focus:outline-none focus:ring-2 focus:ring-offset-2 shadow-sm text-base;
}
.btn-primary {
@apply btn bg-primary-600 hover:bg-primary-700 text-white focus:ring-primary-500 hover:shadow-md active:translate-y-0.5 dark:bg-primary-700 dark:hover:bg-primary-600;
}
.btn-secondary {
@apply btn bg-secondary-600 hover:bg-secondary-700 text-white focus:ring-secondary-500 hover:shadow-md active:translate-y-0.5 dark:bg-secondary-700 dark:hover:bg-secondary-600;
}
.btn-outline {
@apply btn border-2 border-gray-300 dark:border-dark-500 bg-white dark:bg-transparent text-gray-700 dark:text-gray-200 hover:bg-gray-50 hover:border-primary-500 hover:text-primary-600 dark:hover:bg-dark-700 dark:hover:border-primary-400 dark:hover:text-primary-400 focus:ring-gray-500 active:translate-y-0.5;
}
.card {
@apply bg-white shadow-md dark:bg-dark-700 rounded-lg overflow-hidden border border-gray-200 dark:border-dark-600 hover:shadow-lg transition-shadow duration-300;
}
.gradient-text {
@apply text-transparent bg-clip-text bg-gradient-to-r from-primary-600 to-secondary-600 dark:from-primary-500 dark:to-secondary-500 font-bold;
}
.node-tooltip {
@apply max-w-xs p-3 bg-white text-gray-800 dark:bg-dark-800 dark:text-white rounded-lg shadow-lg text-sm z-50 border border-gray-200 dark:border-dark-600;
}
.mindmap-node {
@apply cursor-pointer transition-all duration-200 hover:shadow-lg border-2 border-gray-200 dark:border-dark-600;
}
/* Mindmap-spezifische Stile */
.mindmap-container {
@apply bg-gray-50/80 dark:bg-dark-800/80 rounded-lg p-4 shadow-inner;
}
.mindmap-node-root {
@apply bg-primary-100 dark:bg-primary-900 text-primary-900 dark:text-primary-100 border-primary-300 dark:border-primary-700;
}
.mindmap-node-branch {
@apply bg-secondary-100 dark:bg-secondary-900 text-secondary-900 dark:text-secondary-100 border-secondary-300 dark:border-secondary-700;
}
.mindmap-node-leaf {
@apply bg-gray-100 dark:bg-dark-700 text-gray-800 dark:text-gray-200 border-gray-300 dark:border-dark-500;
}
.mindmap-link {
@apply stroke-gray-400 dark:stroke-gray-500 stroke-[2];
}
.mindmap-link-active {
@apply stroke-primary-500 dark:stroke-primary-400 stroke-[3];
}
.form-group {
@apply mb-4;
}
.form-control {
@apply w-full;
}
.ascii-art {
@apply font-mono text-xs leading-none whitespace-pre tracking-tight select-none text-primary-700 dark:text-primary-400 opacity-80 dark:opacity-60 font-bold;
}
/* Verbesserte Formulareingabefelder */
.form-input,
.form-textarea,
.form-select {
@apply w-full rounded-md border border-gray-300 bg-white px-4 py-2 text-gray-800 shadow-sm
focus:border-primary-500 focus:ring focus:ring-primary-500 focus:ring-opacity-50
dark:border-dark-500 dark:bg-dark-700 dark:text-gray-100 dark:focus:border-primary-400 dark:focus:ring-primary-400;
}
.form-input-lg {
@apply py-3 text-lg;
}
.form-input-sm {
@apply py-1 text-sm;
}
.form-checkbox,
.form-radio {
@apply h-5 w-5 rounded border-gray-300 text-primary-600 shadow-sm
focus:border-primary-500 focus:ring focus:ring-primary-500 focus:ring-opacity-50
dark:border-dark-500 dark:bg-dark-700 dark:focus:border-primary-400 dark:focus:ring-primary-400;
}
.form-checkbox {
@apply rounded;
}
.form-radio {
@apply rounded-full;
}
.form-error {
@apply mt-1 text-sm text-red-600 dark:text-red-400;
}
}
@layer utilities {
.tech-gradient {
@apply bg-gradient-to-r from-primary-600 to-secondary-500;
}
.glass-effect {
@apply bg-white/95 backdrop-blur-md border border-gray-200 shadow-md dark:bg-dark-800/90 dark:border-dark-700/50 dark:shadow-xl;
}
.focus-ring {
@apply focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:focus:ring-primary-400 dark:focus:ring-offset-dark-800;
}
.input-focus {
@apply focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 dark:focus:ring-primary-400 dark:focus:border-primary-400;
}
}
/* Form validation styles */
.is-valid {
@apply border-green-500 dark:border-green-400;
}
.is-invalid {
@apply border-red-500 dark:border-red-400;
}
.is-valid:focus {
@apply ring-green-500/30 border-green-500 dark:ring-green-400/30 dark:border-green-400;
}
.is-invalid:focus {
@apply ring-red-500/30 border-red-500 dark:ring-red-400/30 dark:border-red-400;
}
.form-hint {
@apply text-xs text-gray-500 dark:text-gray-400 mt-1;
}
.form-label {
@apply block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1;
}
.input-icon {
@apply absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-500 dark:text-gray-400;
}
.input-with-icon {
@apply pl-10;
}

1644
static/css/style.css Normal file

File diff suppressed because it is too large Load Diff

6
static/css/tailwind.min.css vendored Normal file
View File

@@ -0,0 +1,6 @@
/**
* Failed to bundle using Rollup v2.79.2: the file imports a not supported node.js built-in module "fs".
* If you believe this to be an issue with jsDelivr, and not with the package itself, please open an issue at https://github.com/jsdelivr/jsdelivr
*/
throw new Error('Failed to bundle using Rollup v2.79.2: the file imports a not supported node.js built-in module "fs". If you believe this to be an issue with jsDelivr, and not with the package itself, please open an issue at https://github.com/jsdelivr/jsdelivr');

526
static/d3-extensions.js vendored Normal file
View File

@@ -0,0 +1,526 @@
/**
* D3.js Erweiterungen für verbesserte Mindmap-Funktionalität
* Diese Datei enthält zusätzliche Hilfsfunktionen und Erweiterungen für D3.js
*/
class D3Extensions {
/**
* Erstellt einen verbesserten radialen Farbverlauf
* @param {Object} defs - Das D3 defs Element
* @param {string} id - ID für den Gradienten
* @param {string} baseColor - Grundfarbe in hexadezimal oder RGB
* @returns {Object} - Das erstellte Gradient-Element
*/
static createEnhancedRadialGradient(defs, id, baseColor) {
// Farben berechnen
const d3Color = d3.color(baseColor);
const lightColor = d3Color.brighter(0.7);
const darkColor = d3Color.darker(0.3);
const midColor = d3Color;
// Gradient erstellen
const gradient = defs.append('radialGradient')
.attr('id', id)
.attr('cx', '30%')
.attr('cy', '30%')
.attr('r', '70%');
// Farbstops hinzufügen für realistischeren Verlauf
gradient.append('stop')
.attr('offset', '0%')
.attr('stop-color', lightColor.formatHex());
gradient.append('stop')
.attr('offset', '50%')
.attr('stop-color', midColor.formatHex());
gradient.append('stop')
.attr('offset', '100%')
.attr('stop-color', darkColor.formatHex());
return gradient;
}
/**
* Erstellt einen Glüheffekt-Filter
* @param {Object} defs - D3-Referenz auf den defs-Bereich
* @param {String} id - ID des Filters
* @param {String} color - Farbe des Glüheffekts (Hex-Code)
* @param {Number} strength - Stärke des Glüheffekts
* @returns {Object} D3-Referenz auf den erstellten Filter
*/
static createGlowFilter(defs, id, color = '#b38fff', strength = 5) {
const filter = defs.append('filter')
.attr('id', id)
.attr('x', '-50%')
.attr('y', '-50%')
.attr('width', '200%')
.attr('height', '200%');
// Unschärfe-Effekt
filter.append('feGaussianBlur')
.attr('in', 'SourceGraphic')
.attr('stdDeviation', strength)
.attr('result', 'blur');
// Farbverstärkung für den Glüheffekt
filter.append('feColorMatrix')
.attr('in', 'blur')
.attr('type', 'matrix')
.attr('values', '0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 18 -7')
.attr('result', 'glow');
// Farbflut mit der angegebenen Farbe
filter.append('feFlood')
.attr('flood-color', color)
.attr('flood-opacity', '0.7')
.attr('result', 'color');
// Zusammensetzen des Glüheffekts mit der Farbe
filter.append('feComposite')
.attr('in', 'color')
.attr('in2', 'glow')
.attr('operator', 'in')
.attr('result', 'glow-color');
// Zusammenfügen aller Ebenen
const feMerge = filter.append('feMerge');
feMerge.append('feMergeNode')
.attr('in', 'glow-color');
feMerge.append('feMergeNode')
.attr('in', 'SourceGraphic');
return filter;
}
/**
* Berechnet eine konsistente Farbe aus einem String
* @param {string} str - Eingabestring
* @returns {string} - Generierte Farbe als Hex-String
*/
static stringToColor(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
}
// Basis-Farbpalette für konsistente Farben
const colorPalette = [
"#4299E1", // Blau
"#9F7AEA", // Lila
"#ED64A6", // Pink
"#48BB78", // Grün
"#ECC94B", // Gelb
"#F56565", // Rot
"#38B2AC", // Türkis
"#ED8936", // Orange
"#667EEA", // Indigo
];
// Farbe aus der Palette wählen basierend auf dem Hash
const colorIndex = Math.abs(hash) % colorPalette.length;
return colorPalette[colorIndex];
}
/**
* Erstellt einen Schatteneffekt-Filter
* @param {Object} defs - D3-Referenz auf den defs-Bereich
* @param {String} id - ID des Filters
* @returns {Object} D3-Referenz auf den erstellten Filter
*/
static createShadowFilter(defs, id) {
const filter = defs.append('filter')
.attr('id', id)
.attr('x', '-50%')
.attr('y', '-50%')
.attr('width', '200%')
.attr('height', '200%');
// Einfacher Schlagschatten
filter.append('feDropShadow')
.attr('dx', 0)
.attr('dy', 4)
.attr('stdDeviation', 4)
.attr('flood-color', 'rgba(0, 0, 0, 0.3)');
return filter;
}
/**
* Erstellt einen Glasmorphismus-Effekt-Filter
* @param {Object} defs - D3-Referenz auf den defs-Bereich
* @param {String} id - ID des Filters
* @returns {Object} D3-Referenz auf den erstellten Filter
*/
static createGlassMorphismFilter(defs, id) {
const filter = defs.append('filter')
.attr('id', id)
.attr('x', '-50%')
.attr('y', '-50%')
.attr('width', '200%')
.attr('height', '200%');
// Hintergrund-Unschärfe für den Glaseffekt
filter.append('feGaussianBlur')
.attr('in', 'SourceGraphic')
.attr('stdDeviation', 8)
.attr('result', 'blur');
// Hellere Farbe für den Glaseffekt
filter.append('feColorMatrix')
.attr('in', 'blur')
.attr('type', 'matrix')
.attr('values', '1 0 0 0 0.1 0 1 0 0 0.1 0 0 1 0 0.1 0 0 0 0.6 0')
.attr('result', 'glass');
// Überlagerung mit dem Original
const feMerge = filter.append('feMerge');
feMerge.append('feMergeNode')
.attr('in', 'glass');
feMerge.append('feMergeNode')
.attr('in', 'SourceGraphic');
return filter;
}
/**
* Erstellt einen verstärkten Glasmorphismus-Effekt mit Farbverlauf
* @param {Object} defs - D3-Referenz auf den defs-Bereich
* @param {String} id - ID des Filters
* @param {String} color1 - Erste Farbe des Verlaufs (Hex-Code)
* @param {String} color2 - Zweite Farbe des Verlaufs (Hex-Code)
* @returns {Object} D3-Referenz auf den erstellten Filter
*/
static createEnhancedGlassMorphismFilter(defs, id, color1 = '#b38fff', color2 = '#58a9ff') {
// Farbverlauf für den Glaseffekt definieren
const gradientId = `gradient-${id}`;
const gradient = defs.append('linearGradient')
.attr('id', gradientId)
.attr('x1', '0%')
.attr('y1', '0%')
.attr('x2', '100%')
.attr('y2', '100%');
gradient.append('stop')
.attr('offset', '0%')
.attr('stop-color', color1)
.attr('stop-opacity', '0.3');
gradient.append('stop')
.attr('offset', '100%')
.attr('stop-color', color2)
.attr('stop-opacity', '0.3');
// Filter erstellen
const filter = defs.append('filter')
.attr('id', id)
.attr('x', '-50%')
.attr('y', '-50%')
.attr('width', '200%')
.attr('height', '200%');
// Hintergrund-Unschärfe
filter.append('feGaussianBlur')
.attr('in', 'SourceGraphic')
.attr('stdDeviation', 6)
.attr('result', 'blur');
// Farbverlauf einfügen
const feImage = filter.append('feImage')
.attr('xlink:href', `#${gradientId}`)
.attr('result', 'gradient')
.attr('x', '0%')
.attr('y', '0%')
.attr('width', '100%')
.attr('height', '100%')
.attr('preserveAspectRatio', 'none');
// Zusammenfügen aller Ebenen
const feMerge = filter.append('feMerge');
feMerge.append('feMergeNode')
.attr('in', 'blur');
feMerge.append('feMergeNode')
.attr('in', 'gradient');
feMerge.append('feMergeNode')
.attr('in', 'SourceGraphic');
return filter;
}
/**
* Erstellt einen 3D-Glaseffekt mit verbesserter Tiefe und Reflexionen
* @param {Object} defs - D3-Referenz auf den defs-Bereich
* @param {String} id - ID des Filters
* @returns {Object} D3-Referenz auf den erstellten Filter
*/
static create3DGlassEffect(defs, id) {
const filter = defs.append('filter')
.attr('id', id)
.attr('x', '-50%')
.attr('y', '-50%')
.attr('width', '200%')
.attr('height', '200%');
// Farbmatrix für Transparenz
filter.append('feColorMatrix')
.attr('type', 'matrix')
.attr('in', 'SourceGraphic')
.attr('values', '1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0.7 0')
.attr('result', 'transparent');
// Hintergrund-Unschärfe für Tiefe
filter.append('feGaussianBlur')
.attr('in', 'transparent')
.attr('stdDeviation', '4')
.attr('result', 'blurred');
// Lichtquelle und Schattierung hinzufügen
const lightSource = filter.append('feSpecularLighting')
.attr('in', 'blurred')
.attr('surfaceScale', '6')
.attr('specularConstant', '1')
.attr('specularExponent', '30')
.attr('lighting-color', '#ffffff')
.attr('result', 'specular');
lightSource.append('fePointLight')
.attr('x', '100')
.attr('y', '100')
.attr('z', '200');
// Lichtreflexion verstärken
filter.append('feComposite')
.attr('in', 'specular')
.attr('in2', 'SourceGraphic')
.attr('operator', 'in')
.attr('result', 'specularHighlight');
// Inneren Schatten erzeugen
const innerShadow = filter.append('feOffset')
.attr('in', 'SourceAlpha')
.attr('dx', '0')
.attr('dy', '1')
.attr('result', 'offsetblur');
innerShadow.append('feGaussianBlur')
.attr('in', 'offsetblur')
.attr('stdDeviation', '2')
.attr('result', 'innerShadow');
filter.append('feComposite')
.attr('in', 'innerShadow')
.attr('in2', 'SourceGraphic')
.attr('operator', 'out')
.attr('result', 'innerShadowEffect');
// Schichten kombinieren
const feMerge = filter.append('feMerge');
feMerge.append('feMergeNode')
.attr('in', 'blurred');
feMerge.append('feMergeNode')
.attr('in', 'innerShadowEffect');
feMerge.append('feMergeNode')
.attr('in', 'specularHighlight');
feMerge.append('feMergeNode')
.attr('in', 'SourceGraphic');
return filter;
}
/**
* Fügt einen Partikelsystem-Effekt für interaktive Knoten hinzu
* @param {Object} parent - Das übergeordnete SVG-Element
* @param {number} x - X-Koordinate des Zentrums
* @param {number} y - Y-Koordinate des Zentrums
* @param {string} color - Partikelfarbe (Hex-Code)
* @param {number} count - Anzahl der Partikel
*/
static createParticleEffect(parent, x, y, color = '#b38fff', count = 5) {
const particles = [];
for (let i = 0; i < count; i++) {
const particle = parent.append('circle')
.attr('cx', x)
.attr('cy', y)
.attr('r', 0)
.attr('fill', color)
.style('opacity', 0.8);
particles.push(particle);
// Partikel animieren
animateParticle(particle);
}
function animateParticle(particle) {
// Zufällige Richtung und Geschwindigkeit
const angle = Math.random() * Math.PI * 2;
const speed = 1 + Math.random() * 2;
const distance = 20 + Math.random() * 30;
// Zielposition berechnen
const targetX = x + Math.cos(angle) * distance;
const targetY = y + Math.sin(angle) * distance;
// Animation mit zufälliger Dauer
const duration = 1000 + Math.random() * 500;
particle
.attr('r', 0)
.style('opacity', 0.8)
.transition()
.duration(duration)
.attr('cx', targetX)
.attr('cy', targetY)
.attr('r', 2 + Math.random() * 3)
.style('opacity', 0)
.on('end', function() {
// Partikel entfernen
particle.remove();
});
}
}
/**
* Führt eine Pulsanimation auf einem Knoten durch
* @param {Object} node - D3-Knoten-Selektion
* @returns {void}
*/
static pulseAnimation(node) {
if (!node) return;
const circle = node.select('circle');
const originalRadius = parseFloat(circle.attr('r'));
const originalFill = circle.attr('fill');
// Pulsanimation
circle
.transition()
.duration(400)
.attr('r', originalRadius * 1.3)
.attr('fill', '#b38fff')
.transition()
.duration(400)
.attr('r', originalRadius)
.attr('fill', originalFill);
}
/**
* Berechnet eine adaptive Schriftgröße basierend auf der Textlänge
* @param {string} text - Der anzuzeigende Text
* @param {number} maxSize - Maximale Schriftgröße in Pixel
* @param {number} minSize - Minimale Schriftgröße in Pixel
* @returns {number} - Die berechnete Schriftgröße
*/
static getAdaptiveFontSize(text, maxSize = 14, minSize = 10) {
if (!text) return maxSize;
// Linear die Schriftgröße basierend auf der Textlänge anpassen
const length = text.length;
if (length <= 6) return maxSize;
if (length >= 20) return minSize;
// Lineare Interpolation
const factor = (length - 6) / (20 - 6);
return maxSize - factor * (maxSize - minSize);
}
/**
* Fügt einen Pulsierenden Effekt zu einer Selektion hinzu
* @param {Object} selection - D3-Selektion
* @param {number} duration - Dauer eines Puls-Zyklus in ms
* @param {number} minOpacity - Minimale Opazität
* @param {number} maxOpacity - Maximale Opazität
*/
static addPulseEffect(selection, duration = 1500, minOpacity = 0.4, maxOpacity = 0.9) {
function pulse() {
selection
.transition()
.duration(duration / 2)
.style('opacity', minOpacity)
.transition()
.duration(duration / 2)
.style('opacity', maxOpacity)
.on('end', pulse);
}
// Initialen Stil setzen
selection.style('opacity', maxOpacity);
// Pulsanimation starten
pulse();
}
/**
* Verarbeitet Daten aus der Datenbank für die Mindmap-Visualisierung
* @param {Array} databaseNodes - Knotendaten aus der Datenbank
* @param {Array} links - Verbindungsdaten oder null für automatische Extraktion
* @returns {Object} Aufbereitete Daten für D3.js
*/
static processDbNodesForVisualization(databaseNodes, links = null) {
// Überprüfe, ob Daten vorhanden sind
if (!databaseNodes || databaseNodes.length === 0) {
console.warn('Keine Knotendaten zum Verarbeiten vorhanden');
return { nodes: [], links: [] };
}
// Knoten mit D3-Kompatiblem Format erstellen
const nodes = databaseNodes.map(node => {
// Farbgenerierung, falls keine vorhanden
const nodeColor = node.color_code ||
node.color ||
D3Extensions.stringToColor(node.name || 'default');
return {
id: node.id,
name: node.name,
description: node.description || '',
thought_count: node.thought_count || 0,
color: nodeColor,
// Zusätzliche Attribute
category_id: node.category_id,
is_public: node.is_public !== undefined ? node.is_public : true,
// Position, falls vorhanden
x: node.x_position,
y: node.y_position,
// Größe, falls vorhanden
scale: node.scale || 1.0
};
});
// Verbindungen verarbeiten
let processedLinks = [];
if (links && Array.isArray(links)) {
// Verwende übergebene Verbindungen
processedLinks = links.map(link => {
return {
source: link.source,
target: link.target,
// Zusätzliche Attribute
type: link.type || 'default',
strength: link.strength || 1
};
});
} else {
// Extrahiere Verbindungen aus den Knoten
databaseNodes.forEach(node => {
if (node.connections && Array.isArray(node.connections)) {
node.connections.forEach(conn => {
processedLinks.push({
source: node.id,
target: conn.target,
type: conn.type || 'default',
strength: conn.strength || 1
});
});
}
});
}
return { nodes, links: processedLinks };
}
}
// Globale Verfügbarkeit sicherstellen
window.D3Extensions = D3Extensions;

BIN
static/example.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

35
static/fonts/inter.css Normal file
View File

@@ -0,0 +1,35 @@
/* Inter font - Local version */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 300;
src: url('../fonts/inter-light.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
src: url('../fonts/inter-regular.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 500;
src: url('../fonts/inter-medium.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
src: url('../fonts/inter-semibold.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
src: url('../fonts/inter-bold.woff2') format('woff2');
}

View File

@@ -0,0 +1,21 @@
/* JetBrains Mono font - Local version */
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 400;
src: url('../fonts/jetbrainsmono-regular.woff2') format('woff2');
}
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 500;
src: url('../fonts/jetbrainsmono-medium.woff2') format('woff2');
}
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 700;
src: url('../fonts/jetbrainsmono-bold.woff2') format('woff2');
}

View File

@@ -0,0 +1,11 @@
<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>

After

Width:  |  Height:  |  Size: 583 B

54
static/img/favicon-gen.py Normal file
View File

@@ -0,0 +1,54 @@
#!/usr/bin/env python3
"""
Generate favicon.ico from SVG using cairosvg and PIL
"""
import os
import io
from cairosvg import svg2png
from PIL import Image
# Verzeichnis dieses Skripts
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
def svg_to_ico(svg_path, ico_path, sizes=[16, 32, 48, 64, 128, 256]):
"""Convert SVG to multi-size ICO file"""
img_io = io.BytesIO()
# Höchste Auflösung für Zwischenspeicherung
max_size = max(sizes)
# SVG in PNG konvertieren
with open(svg_path, 'rb') as svg_file:
svg_data = svg_file.read()
svg2png(bytestring=svg_data, write_to=img_io, output_width=max_size, output_height=max_size)
# PNG in verschiedene Größen konvertieren
img = Image.open(img_io)
# Alle Größen für das ICO-Format vorbereiten
img_list = []
for size in sizes:
resized_img = img.resize((size, size), Image.LANCZOS)
img_list.append(resized_img)
# ICO-Datei speichern
img_list[0].save(
ico_path,
format='ICO',
sizes=[(img.width, img.height) for img in img_list],
append_images=img_list[1:]
)
print(f"Favicon {ico_path} wurde erstellt!")
# 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')
)

21
static/img/favicon.svg Normal file
View File

@@ -0,0 +1,21 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="512" height="512" rx="128" fill="url(#paint0_linear)" />
<path d="M143.5 384V128H180.5L256.5 270L332.5 128H369.5V384H328.5V196L256.5 332H256L183.5 196V384H143.5Z" fill="white"/>
<circle cx="143.5" cy="128" r="20" fill="#a040ff" />
<circle cx="256.5" cy="270" r="25" fill="#a040ff" />
<circle cx="369.5" cy="128" r="20" fill="#a040ff" />
<circle cx="143.5" cy="384" r="20" fill="#4080ff" />
<circle cx="369.5" cy="384" r="20" fill="#4080ff" />
<path d="M143.5 128L183.5 196" stroke="white" stroke-width="4" stroke-linecap="round" />
<path d="M256.5 270L183.5 196" stroke="white" stroke-width="4" stroke-linecap="round" />
<path d="M256.5 270L328.5 196" stroke="white" stroke-width="4" stroke-linecap="round" />
<path d="M369.5 128L328.5 196" stroke="white" stroke-width="4" stroke-linecap="round" />
<path d="M183.5 196L143.5 384" stroke="white" stroke-width="4" stroke-linecap="round" />
<path d="M328.5 196L369.5 384" stroke="white" stroke-width="4" stroke-linecap="round" />
<defs>
<linearGradient id="paint0_linear" x1="0" y1="0" x2="512" y2="512" gradientUnits="userSpaceOnUse">
<stop stop-color="#205cf5" />
<stop offset="1" stop-color="#8020f5" />
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,29 @@
<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>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,59 @@
<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>

After

Width:  |  Height:  |  Size: 3.0 KiB

5
static/js/alpine.min.js vendored Normal file

File diff suppressed because one or more lines are too long

235
static/js/main.js Normal file
View File

@@ -0,0 +1,235 @@
/**
* MindMap - Hauptdatei für globale JavaScript-Funktionen
*/
/**
* Hauptmodul für die MindMap-Anwendung
* Verwaltet die globale Anwendungslogik
*/
/**
* Hauptobjekt der MindMap-Anwendung
*/
const MindMap = {
// App-Status
initialized: false,
darkMode: document.documentElement.classList.contains('dark'),
pageInitializers: {},
currentPage: null,
/**
* Initialisiert die MindMap-Anwendung
*/
init() {
if (this.initialized) return;
// Setze currentPage erst jetzt, wenn DOM garantiert geladen ist
this.currentPage = document.body && document.body.dataset ? document.body.dataset.page : null;
console.log('MindMap-Anwendung wird initialisiert...');
// Initialisiere den ChatGPT-Assistenten
if (typeof ChatGPTAssistant !== 'undefined') {
const assistant = new ChatGPTAssistant();
assistant.init();
// Speichere als Teil von MindMap
this.assistant = assistant;
}
// Seiten-spezifische Initialisierer aufrufen
if (this.currentPage && this.pageInitializers[this.currentPage]) {
this.pageInitializers[this.currentPage]();
}
// Event-Listener einrichten
this.setupEventListeners();
// Dunkel-/Hellmodus aus LocalStorage wiederherstellen
if (localStorage.getItem('darkMode') === 'dark') {
document.documentElement.classList.add('dark');
this.darkMode = true;
}
// Mindmap initialisieren, falls auf der richtigen Seite
this.initializeMindmap();
this.initialized = true;
},
/**
* Initialisiert die D3.js Mindmap-Visualisierung
*/
initializeMindmap() {
// Prüfe, ob wir auf der Mindmap-Seite sind
const mindmapContainer = document.getElementById('mindmap-container');
if (!mindmapContainer) return;
try {
console.log('Initialisiere Mindmap...');
// Prüfe, ob MindMapVisualization geladen ist
if (typeof MindMapVisualization === 'undefined') {
console.error('MindMapVisualization-Klasse ist nicht definiert!');
return;
}
// Initialisiere die Mindmap
const mindmap = new MindMapVisualization('#mindmap-container', {
height: mindmapContainer.clientHeight || 600,
nodeRadius: 18,
selectedNodeRadius: 24,
linkDistance: 150,
onNodeClick: this.handleNodeClick.bind(this)
});
// Globale Referenz für andere Module
window.mindmapInstance = mindmap;
// Event-Listener für Zoom-Buttons
const zoomInBtn = document.getElementById('zoom-in-btn');
if (zoomInBtn) {
zoomInBtn.addEventListener('click', () => {
const svg = d3.select('#mindmap-container svg');
const currentZoom = d3.zoomTransform(svg.node());
const newScale = currentZoom.k * 1.3;
svg.transition().duration(300).call(
d3.zoom().transform,
d3.zoomIdentity.translate(currentZoom.x, currentZoom.y).scale(newScale)
);
});
}
const zoomOutBtn = document.getElementById('zoom-out-btn');
if (zoomOutBtn) {
zoomOutBtn.addEventListener('click', () => {
const svg = d3.select('#mindmap-container svg');
const currentZoom = d3.zoomTransform(svg.node());
const newScale = currentZoom.k / 1.3;
svg.transition().duration(300).call(
d3.zoom().transform,
d3.zoomIdentity.translate(currentZoom.x, currentZoom.y).scale(newScale)
);
});
}
const centerBtn = document.getElementById('center-btn');
if (centerBtn) {
centerBtn.addEventListener('click', () => {
const svg = d3.select('#mindmap-container svg');
svg.transition().duration(500).call(
d3.zoom().transform,
d3.zoomIdentity.scale(1)
);
});
}
// Event-Listener für Add-Thought-Button
const addThoughtBtn = document.getElementById('add-thought-btn');
if (addThoughtBtn) {
addThoughtBtn.addEventListener('click', () => {
this.showAddThoughtDialog();
});
}
// Event-Listener für Connect-Button
const connectBtn = document.getElementById('connect-btn');
if (connectBtn) {
connectBtn.addEventListener('click', () => {
this.showConnectDialog();
});
}
} catch (error) {
console.error('Fehler bei der Initialisierung der Mindmap:', error);
}
},
/**
* Handler für Klick auf einen Knoten in der Mindmap
* @param {Object} node - Der angeklickte Knoten
*/
handleNodeClick(node) {
console.log('Knoten wurde angeklickt:', node);
// Hier könnte man Logik hinzufügen, um Detailinformationen anzuzeigen
// oder den ausgewählten Knoten hervorzuheben
const detailsContainer = document.getElementById('node-details');
if (detailsContainer) {
detailsContainer.innerHTML = `
<div class="p-4">
<h3 class="text-xl font-bold mb-2">${node.name}</h3>
<p class="text-gray-300 mb-4">${node.description || 'Keine Beschreibung verfügbar.'}</p>
<div class="flex items-center justify-between">
<span class="text-sm">
<i class="fas fa-brain mr-1"></i> ${node.thought_count || 0} Gedanken
</span>
<button class="px-3 py-1 bg-purple-600 bg-opacity-30 rounded-lg text-sm">
<i class="fas fa-plus mr-1"></i> Gedanke hinzufügen
</button>
</div>
</div>
`;
// Button zum Hinzufügen eines Gedankens
const addThoughtBtn = detailsContainer.querySelector('button');
addThoughtBtn.addEventListener('click', () => {
this.showAddThoughtDialog(node);
});
}
},
/**
* Dialog zum Hinzufügen eines neuen Knotens
*/
showAddNodeDialog() {
// Diese Funktionalität würde in einer vollständigen Implementierung eingebunden werden
alert('Diese Funktion steht bald zur Verfügung!');
},
/**
* Dialog zum Hinzufügen eines neuen Gedankens zu einem Knoten
*/
showAddThoughtDialog(node) {
// Diese Funktionalität würde in einer vollständigen Implementierung eingebunden werden
alert('Diese Funktion steht bald zur Verfügung!');
},
/**
* Dialog zum Verbinden von Knoten
*/
showConnectDialog() {
// Diese Funktionalität würde in einer vollständigen Implementierung eingebunden werden
alert('Diese Funktion steht bald zur Verfügung!');
},
/**
* Richtet Event-Listener für die Benutzeroberfläche ein
*/
setupEventListeners() {
// Event-Listener für Dark Mode-Wechsel
document.addEventListener('darkModeToggled', (event) => {
this.darkMode = event.detail.isDark;
});
// Responsive Anpassungen bei Fenstergröße
window.addEventListener('resize', () => {
if (window.mindmapInstance) {
const container = document.getElementById('mindmap-container');
if (container) {
window.mindmapInstance.width = container.clientWidth;
window.mindmapInstance.height = container.clientHeight;
}
}
});
}
};
window.MindMap = MindMap;
document.addEventListener('DOMContentLoaded', function() {
// Initialisiere die Anwendung
MindMap.init();
// Wende Dunkel-/Hellmodus an
const isDarkMode = localStorage.getItem('darkMode') === 'dark';
document.documentElement.classList.toggle('dark', isDarkMode);
});

214
static/js/mindmap-init.js Normal file
View File

@@ -0,0 +1,214 @@
/**
* 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

@@ -0,0 +1,546 @@
/**
* ChatGPT Assistent Modul
* Verwaltet die Interaktion mit der OpenAI API und die Benutzeroberfläche des Assistenten
*/
class ChatGPTAssistant {
constructor() {
this.messages = [];
this.isOpen = false;
this.isLoading = false;
this.container = null;
this.chatHistory = null;
this.inputField = null;
this.suggestionArea = null;
this.maxRetries = 2;
this.retryCount = 0;
this.markdownParser = null;
this.initializeMarkdownParser();
}
/**
* Initialisiert den Markdown-Parser
*/
async initializeMarkdownParser() {
// Dynamisch marked.js laden, wenn noch nicht vorhanden
if (!window.marked) {
try {
// Prüfen, ob marked.js bereits im Dokument geladen ist
if (!document.querySelector('script[src*="marked"]')) {
// Falls nicht, Script-Tag erstellen und einfügen
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/marked/marked.min.js';
script.async = true;
// Promise erstellen, das resolved wird, wenn das Script geladen wurde
await new Promise((resolve, reject) => {
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
console.log('Marked.js erfolgreich geladen');
}
// Marked konfigurieren
this.markdownParser = window.marked;
this.markdownParser.setOptions({
gfm: true,
breaks: true,
sanitize: true,
smartLists: true,
smartypants: true
});
} catch (error) {
console.error('Fehler beim Laden von marked.js:', error);
// Fallback-Parser, der nur einfache Absätze erkennt
this.markdownParser = {
parse: (text) => {
return text.split('\n').map(line => {
if (line.trim() === '') return '<br>';
return `<p>${line}</p>`;
}).join('');
}
};
}
} else {
// Marked ist bereits geladen
this.markdownParser = window.marked;
}
}
/**
* Initialisiert den Assistenten und fügt die UI zum DOM hinzu
*/
init() {
// Assistent-Container erstellen
this.createAssistantUI();
// Event-Listener hinzufügen
this.setupEventListeners();
// Ersten Willkommensnachricht anzeigen
this.addMessage("assistant", "Hallo! Ich bin dein KI-Assistent (4o-mini) und habe Zugriff auf die Wissensdatenbank. Wie kann ich dir helfen?\n\nDu kannst mir Fragen über:\n- **Gedanken** in der Datenbank\n- **Kategorien** und Wissenschaftsbereiche\n- **Mindmaps** und Wissensverknüpfungen\n\nstellen.");
// Vorschläge anzeigen
this.showSuggestions([
"Zeige mir Gedanken zur künstlichen Intelligenz",
"Welche Kategorien gibt es in der Datenbank?",
"Suche nach Mindmaps zum Thema Informatik"
]);
console.log('KI-Assistent initialisiert!');
}
/**
* Erstellt die UI-Elemente für den Assistenten
*/
createAssistantUI() {
// Hauptcontainer erstellen
this.container = document.createElement('div');
this.container.id = 'chatgpt-assistant';
this.container.className = 'fixed bottom-4 right-4 z-50 flex flex-col';
// Button zum Öffnen/Schließen des Assistenten
const toggleButton = document.createElement('button');
toggleButton.id = 'assistant-toggle';
toggleButton.className = 'ml-auto bg-primary-600 hover:bg-primary-700 text-white rounded-full p-3 shadow-lg transition-all duration-300 mb-2';
toggleButton.innerHTML = '<i class="fas fa-robot text-xl"></i>';
// Chat-Container
const chatContainer = document.createElement('div');
chatContainer.id = 'assistant-chat';
chatContainer.className = 'bg-white dark:bg-dark-800 rounded-lg shadow-xl overflow-hidden transition-all duration-300 w-80 md:w-96 max-h-0 opacity-0';
// Chat-Header
const header = document.createElement('div');
header.className = 'bg-primary-600 text-white p-3 flex items-center justify-between';
header.innerHTML = `
<div class="flex items-center">
<i class="fas fa-robot mr-2"></i>
<span>KI-Assistent (4o-mini)</span>
</div>
<button id="assistant-close" class="text-white hover:text-gray-200">
<i class="fas fa-times"></i>
</button>
`;
// Chat-Verlauf
this.chatHistory = document.createElement('div');
this.chatHistory.id = 'assistant-history';
this.chatHistory.className = 'p-3 overflow-y-auto max-h-96 space-y-3';
// Vorschlagsbereich
this.suggestionArea = document.createElement('div');
this.suggestionArea.id = 'assistant-suggestions';
this.suggestionArea.className = 'px-3 pb-2 flex flex-wrap gap-2 overflow-x-auto hidden';
// Chat-Eingabe
const inputContainer = document.createElement('div');
inputContainer.className = 'border-t border-gray-200 dark:border-dark-600 p-3 flex items-center';
this.inputField = document.createElement('input');
this.inputField.type = 'text';
this.inputField.placeholder = 'Stelle eine Frage zur Wissensdatenbank...';
this.inputField.className = 'flex-1 border border-gray-300 dark:border-dark-600 dark:bg-dark-700 dark:text-white rounded-l-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary-500';
const sendButton = document.createElement('button');
sendButton.id = 'assistant-send';
sendButton.className = 'bg-primary-600 hover:bg-primary-700 text-white px-4 py-2 rounded-r-lg';
sendButton.innerHTML = '<i class="fas fa-paper-plane"></i>';
// Elemente zusammenfügen
inputContainer.appendChild(this.inputField);
inputContainer.appendChild(sendButton);
chatContainer.appendChild(header);
chatContainer.appendChild(this.chatHistory);
chatContainer.appendChild(this.suggestionArea);
chatContainer.appendChild(inputContainer);
this.container.appendChild(toggleButton);
this.container.appendChild(chatContainer);
// Zum DOM hinzufügen
document.body.appendChild(this.container);
}
/**
* Richtet Event-Listener für die Benutzeroberfläche ein
*/
setupEventListeners() {
// Toggle-Button
const toggleButton = document.getElementById('assistant-toggle');
if (toggleButton) {
toggleButton.addEventListener('click', () => this.toggleAssistant());
}
// Schließen-Button
const closeButton = document.getElementById('assistant-close');
if (closeButton) {
closeButton.addEventListener('click', () => this.toggleAssistant(false));
}
// Senden-Button
const sendButton = document.getElementById('assistant-send');
if (sendButton) {
sendButton.addEventListener('click', () => this.sendMessage());
}
// Enter-Taste im Eingabefeld
if (this.inputField) {
this.inputField.addEventListener('keyup', (e) => {
if (e.key === 'Enter') {
this.sendMessage();
}
});
}
// Vorschläge klickbar machen
if (this.suggestionArea) {
this.suggestionArea.addEventListener('click', (e) => {
if (e.target.classList.contains('suggestion-pill')) {
this.inputField.value = e.target.textContent;
this.sendMessage();
}
});
}
}
/**
* Öffnet oder schließt den Assistenten
* @param {boolean} state - Optional: erzwingt einen bestimmten Zustand
*/
toggleAssistant(state = null) {
const chatContainer = document.getElementById('assistant-chat');
if (!chatContainer) return;
this.isOpen = state !== null ? state : !this.isOpen;
if (this.isOpen) {
chatContainer.classList.remove('max-h-0', 'opacity-0');
chatContainer.classList.add('max-h-[32rem]', 'opacity-100');
if (this.inputField) this.inputField.focus();
// Zeige Vorschläge wenn verfügbar
if (this.suggestionArea && this.suggestionArea.children.length > 0) {
this.suggestionArea.classList.remove('hidden');
}
} else {
chatContainer.classList.remove('max-h-[32rem]', 'opacity-100');
chatContainer.classList.add('max-h-0', 'opacity-0');
}
}
/**
* Fügt eine Nachricht zum Chat-Verlauf hinzu
* @param {string} sender - 'user' oder 'assistant'
* @param {string} text - Nachrichtentext
*/
addMessage(sender, text) {
// Nachricht zum Verlauf hinzufügen
this.messages.push({ role: sender, content: text });
// DOM-Element erstellen
const messageEl = document.createElement('div');
messageEl.className = `flex ${sender === 'user' ? 'justify-end' : 'justify-start'}`;
const bubble = document.createElement('div');
bubble.className = sender === 'user'
? 'user-message rounded-lg py-2 px-3 max-w-[85%]'
: 'assistant-message rounded-lg py-2 px-3 max-w-[85%]';
// Nachrichtentext einfügen, falls Markdown-Parser verfügbar, nutzen
if (this.markdownParser) {
bubble.innerHTML = this.markdownParser.parse(text);
} else {
bubble.textContent = text;
}
// Links in der Nachricht klickbar machen
const links = bubble.querySelectorAll('a');
links.forEach(link => {
link.target = '_blank';
link.rel = 'noopener noreferrer';
link.className = 'text-primary-600 dark:text-primary-400 underline';
});
// Code-Blöcke stylen
const codeBlocks = bubble.querySelectorAll('pre');
codeBlocks.forEach(block => {
block.className = 'bg-gray-100 dark:bg-dark-900 p-2 rounded my-2 overflow-x-auto';
});
const inlineCode = bubble.querySelectorAll('code:not(pre code)');
inlineCode.forEach(code => {
code.className = 'bg-gray-100 dark:bg-dark-900 px-1 rounded font-mono text-sm';
});
messageEl.appendChild(bubble);
this.chatHistory.appendChild(messageEl);
// Scrolle zum Ende des Chat-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);
// Eingabefeld zurücksetzen
this.inputField.value = '';
// Ladeindikator anzeigen
this.isLoading = true;
this.showLoadingIndicator();
try {
console.log('Sende Anfrage an KI-Assistent API...');
// Anfrage an den Server senden
const response = await fetch('/api/assistant', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
messages: this.messages
}),
cache: 'no-cache', // Kein Cache verwenden
credentials: 'same-origin', // Cookies senden
timeout: 60000 // 60 Sekunden Timeout
});
// 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
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
}
} 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
*/
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.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';
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);
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
}
/**
* 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();
}
}
}
// Mache die Klasse global verfügbar
window.ChatGPTAssistant = ChatGPTAssistant;

View File

@@ -0,0 +1,551 @@
/**
* Mindmap-Seite JavaScript
* Spezifische Funktionen für die Mindmap-Seite
*/
// Füge das Modul zum globalen MindMap-Objekt hinzu
if (!window.MindMap) {
window.MindMap = {};
}
// Registriere den Initialisierer im MindMap-Objekt
window.MindMap.pageInitializers = window.MindMap.pageInitializers || {};
window.MindMap.pageInitializers.mindmap = initMindmapPage;
// Event-Listener für DOMContentLoaded
document.addEventListener('DOMContentLoaded', function() {
// Prüfe, ob wir auf der Mindmap-Seite sind
if (document.body && document.body.dataset && document.body.dataset.page === 'mindmap') {
initMindmapPage();
}
});
/**
* Initialisiert die Mindmap-Seite
*/
function initMindmapPage() {
console.log('Mindmap-Seite wird initialisiert...');
// 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 = '';
}
}
/**
* Generiert Standarddaten für die Mindmap als Fallback
*/
function generateDefaultData() {
return {
nodes: [
{ id: 'root', name: 'Wissen', description: 'Zentrale Wissensbasis', category: 'Zentral', color_code: '#4299E1' },
{ id: 'philosophy', name: 'Philosophie', description: 'Philosophisches Denken', category: 'Philosophie', color_code: '#9F7AEA', parent_id: 'root' },
{ id: 'science', name: 'Wissenschaft', description: 'Wissenschaftliche Erkenntnisse', category: 'Wissenschaft', color_code: '#48BB78', parent_id: 'root' },
{ id: 'technology', name: 'Technologie', description: 'Technologische Entwicklungen', category: 'Technologie', color_code: '#ED8936', parent_id: 'root' },
{ id: 'arts', name: 'Künste', description: 'Künstlerische Ausdrucksformen', category: 'Künste', color_code: '#ED64A6', parent_id: 'root' },
{ id: 'psychology', name: 'Psychologie', description: 'Menschliches Verhalten und Geist', category: 'Psychologie', color_code: '#4299E1', parent_id: 'root' }
]
};
}
/**
* Rendert die Mindmap mit Cytoscape.js
*/
function renderMindmap(data) {
console.log('Rendere Mindmap mit Daten:', data);
// Konvertiere Backend-Daten in Cytoscape-Format
const elements = convertToCytoscapeFormat(data);
// Leere den Container
cyContainer.innerHTML = '';
// Erstelle Cytoscape-Instanz
mindmap = cytoscape({
container: cyContainer,
elements: elements,
style: [
{
selector: 'node',
style: {
'background-color': 'data(color)',
'label': 'data(name)',
'width': 30,
'height': 30,
'font-size': 12,
'text-valign': 'bottom',
'text-halign': 'center',
'text-margin-y': 8,
'color': document.documentElement.classList.contains('dark') ? '#f1f5f9' : '#334155',
'text-background-color': document.documentElement.classList.contains('dark') ? 'rgba(30, 41, 59, 0.8)' : 'rgba(241, 245, 249, 0.8)',
'text-background-opacity': 0.8,
'text-background-padding': '2px',
'text-background-shape': 'roundrectangle',
'text-wrap': 'ellipsis',
'text-max-width': '100px'
}
},
{
selector: 'edge',
style: {
'width': 2,
'line-color': document.documentElement.classList.contains('dark') ? 'rgba(255, 255, 255, 0.15)' : 'rgba(30, 41, 59, 0.15)',
'target-arrow-color': document.documentElement.classList.contains('dark') ? 'rgba(255, 255, 255, 0.15)' : 'rgba(30, 41, 59, 0.15)',
'curve-style': 'bezier'
}
},
{
selector: 'node:selected',
style: {
'background-color': 'data(color)',
'border-width': 3,
'border-color': '#8b5cf6',
'width': 40,
'height': 40,
'font-size': 14,
'font-weight': 'bold',
'text-background-color': '#8b5cf6',
'text-background-opacity': 0.9
}
}
],
layout: {
name: 'cose',
animate: true,
animationDuration: 800,
nodeDimensionsIncludeLabels: true,
refresh: 30,
randomize: true,
componentSpacing: 100,
nodeRepulsion: 8000,
nodeOverlap: 20,
idealEdgeLength: 200,
edgeElasticity: 100,
nestingFactor: 1.2,
gravity: 80,
fit: true,
padding: 30
}
});
// Event-Listener für Knoteninteraktionen
mindmap.on('tap', 'node', function(evt) {
const node = evt.target;
const nodeData = node.data();
// Update Info-Panel
updateNodeInfoPanel(nodeData);
// Lade verbundene Knoten
updateConnectedNodes(node);
});
// Toolbar-Buttons aktivieren
if (fitButton) {
fitButton.addEventListener('click', () => {
mindmap.fit();
mindmap.center();
});
}
if (resetButton) {
resetButton.addEventListener('click', () => {
mindmap.layout({
name: 'cose',
animate: true,
randomize: true,
fit: true
}).run();
});
}
if (toggleLabelsButton) {
let labelsVisible = true;
toggleLabelsButton.addEventListener('click', () => {
labelsVisible = !labelsVisible;
if (labelsVisible) {
mindmap.style()
.selector('node')
.style('label', 'data(name)')
.update();
} else {
mindmap.style()
.selector('node')
.style('label', '')
.update();
}
});
}
// Dark Mode-Änderungen überwachen
document.addEventListener('darkModeToggled', function(event) {
updateDarkModeStyles(event.detail.isDark);
});
// Initial fit und center
setTimeout(() => {
mindmap.fit();
mindmap.center();
}, 100);
}
/**
* Konvertiert die Backend-Daten ins Cytoscape-Format
*/
function convertToCytoscapeFormat(data) {
const elements = {
nodes: [],
edges: []
};
// Nodes hinzufügen
if (data.nodes && data.nodes.length > 0) {
data.nodes.forEach(node => {
elements.nodes.push({
data: {
id: String(node.id),
name: node.name,
description: node.description || 'Keine Beschreibung verfügbar',
category: node.category || 'Allgemein',
color: node.color_code || getRandomColor()
}
});
// Kante zum Elternknoten hinzufügen (falls vorhanden)
if (node.parent_id) {
elements.edges.push({
data: {
id: `edge-${node.parent_id}-${node.id}`,
source: String(node.parent_id),
target: String(node.id)
}
});
}
});
// Zusätzliche Kanten zwischen Knoten hinzufügen (falls in den Daten vorhanden)
if (data.edges && data.edges.length > 0) {
data.edges.forEach(edge => {
elements.edges.push({
data: {
id: `edge-${edge.source}-${edge.target}`,
source: String(edge.source),
target: String(edge.target)
}
});
});
}
}
return elements;
}
/**
* Aktualisiert das Informations-Panel mit den Knotendaten
*/
function updateNodeInfoPanel(nodeData) {
if (nodeInfoPanel && nodeDescription) {
// Panel anzeigen
nodeInfoPanel.style.display = 'block';
// Titel und Beschreibung aktualisieren
const titleElement = nodeInfoPanel.querySelector('.info-panel-title');
if (titleElement) {
titleElement.textContent = nodeData.name;
}
if (nodeDescription) {
nodeDescription.textContent = nodeData.description || 'Keine Beschreibung verfügbar';
}
}
}
/**
* Aktualisiert die Liste der verbundenen Knoten
*/
function updateConnectedNodes(node) {
if (connectedNodes) {
// Leere den Container
connectedNodes.innerHTML = '';
// Hole verbundene Knoten
const connectedEdges = node.connectedEdges();
if (connectedEdges.length === 0) {
connectedNodes.innerHTML = '<div class="text-sm italic">Keine verbundenen Knoten</div>';
return;
}
// Füge alle verbundenen Knoten hinzu
connectedEdges.forEach(edge => {
const targetNode = edge.target().id() === node.id() ? edge.source() : edge.target();
const targetData = targetNode.data();
const nodeLink = document.createElement('div');
nodeLink.className = 'node-link';
nodeLink.innerHTML = `
<span class="w-2 h-2 rounded-full" style="background-color: ${targetData.color}"></span>
${targetData.name}
`;
// Klick-Event zum Fokussieren des Knotens
nodeLink.addEventListener('click', () => {
mindmap.center(targetNode);
targetNode.select();
updateNodeInfoPanel(targetData);
updateConnectedNodes(targetNode);
});
connectedNodes.appendChild(nodeLink);
});
}
}
/**
* Aktualisiert die Styles bei Dark Mode-Änderungen
*/
function updateDarkModeStyles(isDark) {
if (!mindmap) return;
const textColor = isDark ? '#f1f5f9' : '#334155';
const textBgColor = isDark ? 'rgba(30, 41, 59, 0.8)' : 'rgba(241, 245, 249, 0.8)';
const edgeColor = isDark ? 'rgba(255, 255, 255, 0.15)' : 'rgba(30, 41, 59, 0.15)';
mindmap.style()
.selector('node')
.style({
'color': textColor,
'text-background-color': textBgColor
})
.selector('edge')
.style({
'line-color': edgeColor,
'target-arrow-color': edgeColor
})
.update();
}
/**
* Generiert eine zufällige Farbe
*/
function getRandomColor() {
const colors = [
'#4299E1', // Blau
'#9F7AEA', // Lila
'#48BB78', // Grün
'#ED8936', // Orange
'#ED64A6', // Pink
'#F56565' // Rot
];
return colors[Math.floor(Math.random() * colors.length)];
}
// Initialisiere die Mindmap-Seite
initMindmapPage();

1133
static/js/social.js Normal file

File diff suppressed because it is too large Load Diff

2724
static/js/update_mindmap.js Normal file

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,415 @@
/**
* Vereinfachter Neuronales Netzwerk Hintergrund
* Verwendet Canvas 2D anstelle von WebGL für bessere Leistung
*/
class NeuralNetworkBackground {
constructor() {
// Canvas einrichten
this.canvas = document.createElement('canvas');
this.canvas.id = 'neural-network-background';
this.canvas.style.position = 'fixed';
this.canvas.style.top = '0';
this.canvas.style.left = '0';
this.canvas.style.width = '100%';
this.canvas.style.height = '100%';
this.canvas.style.zIndex = '-10';
this.canvas.style.pointerEvents = 'none';
this.canvas.style.opacity = '1';
this.canvas.style.transition = 'opacity 3.5s ease-in-out';
// Falls Canvas bereits existiert, entfernen
const existingCanvas = document.getElementById('neural-network-background');
if (existingCanvas) {
existingCanvas.remove();
}
// An body anhängen als erstes Kind
if (document.body.firstChild) {
document.body.insertBefore(this.canvas, document.body.firstChild);
} else {
document.body.appendChild(this.canvas);
}
// 2D Context
this.ctx = this.canvas.getContext('2d');
// Eigenschaften
this.nodes = [];
this.connections = [];
this.activeConnections = new Set();
this.animationFrameId = null;
this.isDestroying = false;
// Farben für Dark/Light Mode
this.colors = {
dark: {
background: '#040215',
nodeColor: '#6a5498',
nodePulse: '#9c7fe0',
connectionColor: '#4a3870',
flowColor: '#b47fea'
},
light: {
background: '#f8f9fc',
nodeColor: '#8c6db5',
nodePulse: '#b094dd',
connectionColor: '#9882bd',
flowColor: '#7d5bb5'
}
};
// Aktuelle Farbpalette basierend auf Theme
this.currentColors = document.documentElement.classList.contains('dark')
? this.colors.dark
: this.colors.light;
// Konfiguration
this.config = {
nodeCount: 80, // Anzahl der Knoten
nodeSize: 2.5, // Größe der Knoten
connectionDistance: 150, // Maximale Verbindungsdistanz
connectionOpacity: 0.5, // Erhöht von 0.3 auf 0.5 - Deckkraft der ständigen Verbindungen
animationSpeed: 0.15, // Geschwindigkeit der Animation
flowDensity: 2, // Anzahl aktiver Verbindungen
maxFlowsPerNode: 2, // Maximale Anzahl aktiver Verbindungen pro Knoten
flowDuration: [2000, 5000], // Min/Max Dauer des Flows in ms
nodePulseFrequency: 0.01 // Wie oft Knoten pulsieren
};
// Initialisieren
this.init();
// Event-Listener
window.addEventListener('resize', this.resizeCanvas.bind(this));
console.log('Vereinfachter Neural Network Background initialized');
}
init() {
this.resizeCanvas();
this.createNodes();
this.createConnections();
this.startAnimation();
}
resizeCanvas() {
const pixelRatio = window.devicePixelRatio || 1;
const width = window.innerWidth;
const height = window.innerHeight;
this.canvas.style.width = width + 'px';
this.canvas.style.height = height + 'px';
this.canvas.width = width * pixelRatio;
this.canvas.height = height * pixelRatio;
if (this.ctx) {
this.ctx.scale(pixelRatio, pixelRatio);
}
// Neuberechnung der Knotenpositionen nach Größenänderung
if (this.nodes.length) {
this.createNodes();
this.createConnections();
}
}
createNodes() {
this.nodes = [];
const width = this.canvas.width / (window.devicePixelRatio || 1);
const height = this.canvas.height / (window.devicePixelRatio || 1);
// Cluster-Zentren für realistisches neuronales Netzwerk
const clusterCount = Math.floor(6 + Math.random() * 4);
const clusters = [];
for (let i = 0; i < clusterCount; i++) {
clusters.push({
x: Math.random() * width,
y: Math.random() * height,
radius: 100 + Math.random() * 150
});
}
// Knoten erstellen
for (let i = 0; i < this.config.nodeCount; i++) {
// Wähle zufällig ein Cluster
const cluster = clusters[Math.floor(Math.random() * clusters.length)];
// Erstelle einen Knoten innerhalb des Clusters mit zufälligem Offset
const angle = Math.random() * Math.PI * 2;
const distance = Math.random() * cluster.radius;
const node = {
id: i,
x: cluster.x + Math.cos(angle) * distance,
y: cluster.y + Math.sin(angle) * distance,
size: this.config.nodeSize * (0.8 + Math.random() * 0.4),
speed: {
x: (Math.random() - 0.5) * 0.2,
y: (Math.random() - 0.5) * 0.2
},
lastPulse: 0,
pulseInterval: 5000 + Math.random() * 10000, // Zufälliges Pulsieren
connections: []
};
this.nodes.push(node);
}
}
createConnections() {
this.connections = [];
// Verbindungen zwischen Knoten erstellen
for (let i = 0; i < this.nodes.length; i++) {
const nodeA = this.nodes[i];
for (let j = i + 1; j < this.nodes.length; j++) {
const nodeB = this.nodes[j];
const dx = nodeA.x - nodeB.x;
const dy = nodeA.y - nodeB.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < this.config.connectionDistance) {
const connection = {
id: `${i}-${j}`,
from: i,
to: j,
distance: distance,
opacity: Math.max(0.05, 1 - (distance / this.config.connectionDistance)),
active: false,
flowProgress: 0,
flowDuration: 0,
flowStart: 0
};
this.connections.push(connection);
nodeA.connections.push(connection);
nodeB.connections.push(connection);
}
}
}
}
startAnimation() {
this.animate();
}
animate() {
this.animationFrameId = requestAnimationFrame(this.animate.bind(this));
const now = Date.now();
this.updateNodes(now);
this.updateConnections(now);
this.render(now);
}
updateNodes(now) {
const width = this.canvas.width / (window.devicePixelRatio || 1);
const height = this.canvas.height / (window.devicePixelRatio || 1);
// Knoten bewegen
for (let i = 0; i < this.nodes.length; i++) {
const node = this.nodes[i];
node.x += node.speed.x;
node.y += node.speed.y;
// Begrenzung am Rand
if (node.x < 0 || node.x > width) {
node.speed.x *= -1;
}
if (node.y < 0 || node.y > height) {
node.speed.y *= -1;
}
// Zufällig Richtung ändern
if (Math.random() < 0.01) {
node.speed.x = (Math.random() - 0.5) * 0.2;
node.speed.y = (Math.random() - 0.5) * 0.2;
}
// Zufälliges Pulsieren
if (Math.random() < this.config.nodePulseFrequency && now - node.lastPulse > node.pulseInterval) {
node.lastPulse = now;
}
}
}
updateConnections(now) {
// Update aktive Verbindungen
for (const connectionId of this.activeConnections) {
const connection = this.connections.find(c => c.id === connectionId);
if (!connection) continue;
// Aktualisiere den Flow-Fortschritt
const elapsed = now - connection.flowStart;
const progress = elapsed / connection.flowDuration;
if (progress >= 1) {
// Flow beenden
connection.active = false;
this.activeConnections.delete(connectionId);
} else {
connection.flowProgress = progress;
}
}
// Neue Flows starten, wenn unter dem Limit
if (this.activeConnections.size < this.config.flowDensity) {
// Wähle eine zufällige Verbindung
const availableConnections = this.connections.filter(c => !c.active);
if (availableConnections.length > 0) {
const randomIndex = Math.floor(Math.random() * availableConnections.length);
const connection = availableConnections[randomIndex];
// Aktiviere die Verbindung
connection.active = true;
connection.flowProgress = 0;
connection.flowStart = now;
connection.flowDuration = this.config.flowDuration[0] +
Math.random() * (this.config.flowDuration[1] - this.config.flowDuration[0]);
this.activeConnections.add(connection.id);
}
}
}
render(now) {
// Aktualisiere Farben basierend auf aktuellem Theme
this.currentColors = document.documentElement.classList.contains('dark')
? this.colors.dark
: this.colors.light;
const colors = this.currentColors;
const width = this.canvas.width / (window.devicePixelRatio || 1);
const height = this.canvas.height / (window.devicePixelRatio || 1);
// Hintergrund löschen
this.ctx.fillStyle = colors.background;
this.ctx.fillRect(0, 0, width, height);
// Verbindungen zeichnen (statisch)
this.ctx.strokeStyle = colors.connectionColor;
this.ctx.lineWidth = 1.2;
for (const connection of this.connections) {
const fromNode = this.nodes[connection.from];
const toNode = this.nodes[connection.to];
this.ctx.globalAlpha = connection.opacity * 0.5;
this.ctx.beginPath();
this.ctx.moveTo(fromNode.x, fromNode.y);
this.ctx.lineTo(toNode.x, toNode.y);
this.ctx.stroke();
}
// Aktive Verbindungen zeichnen (Flows)
this.ctx.strokeStyle = colors.flowColor;
this.ctx.lineWidth = 2.5;
for (const connectionId of this.activeConnections) {
const connection = this.connections.find(c => c.id === connectionId);
if (!connection) continue;
const fromNode = this.nodes[connection.from];
const toNode = this.nodes[connection.to];
// Glühen-Effekt
this.ctx.globalAlpha = Math.sin(connection.flowProgress * Math.PI) * 0.8;
// Linie zeichnen
this.ctx.beginPath();
this.ctx.moveTo(fromNode.x, fromNode.y);
this.ctx.lineTo(toNode.x, toNode.y);
this.ctx.stroke();
// Fließendes Partikel
const progress = connection.flowProgress;
const x = fromNode.x + (toNode.x - fromNode.x) * progress;
const y = fromNode.y + (toNode.y - fromNode.y) * progress;
this.ctx.globalAlpha = 0.9;
this.ctx.fillStyle = colors.flowColor;
this.ctx.beginPath();
this.ctx.arc(x, y, 2, 0, Math.PI * 2);
this.ctx.fill();
}
// Knoten zeichnen
for (const node of this.nodes) {
// Pulsierende Knoten
const timeSinceLastPulse = now - node.lastPulse;
const isPulsing = timeSinceLastPulse < 800;
const pulseProgress = isPulsing ? timeSinceLastPulse / 800 : 0;
// Knoten selbst
this.ctx.globalAlpha = 1;
this.ctx.fillStyle = isPulsing
? colors.nodePulse
: colors.nodeColor;
this.ctx.beginPath();
this.ctx.arc(node.x, node.y, node.size + (isPulsing ? 1 * Math.sin(pulseProgress * Math.PI) : 0), 0, Math.PI * 2);
this.ctx.fill();
// Wenn pulsierend, füge einen Glow-Effekt hinzu
if (isPulsing) {
this.ctx.globalAlpha = 0.5 * (1 - pulseProgress);
this.ctx.beginPath();
this.ctx.arc(node.x, node.y, node.size + 5 * pulseProgress, 0, Math.PI * 2);
this.ctx.fill();
}
}
this.ctx.globalAlpha = 1;
}
destroy() {
if (this.isDestroying) return;
this.isDestroying = true;
// Animation stoppen
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId);
}
// Canvas ausblenden
this.canvas.style.opacity = '0';
// Nach Übergang entfernen
setTimeout(() => {
if (this.canvas && this.canvas.parentNode) {
this.canvas.parentNode.removeChild(this.canvas);
}
}, 3500);
}
hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
}
// Initialisiert den Hintergrund, sobald die Seite geladen ist
document.addEventListener('DOMContentLoaded', () => {
window.neuralBackground = new NeuralNetworkBackground();
// Theme-Wechsel-Event-Listener
document.addEventListener('theme-changed', () => {
if (window.neuralBackground) {
window.neuralBackground.currentColors = document.documentElement.classList.contains('dark')
? window.neuralBackground.colors.dark
: window.neuralBackground.colors.light;
}
});
});

508
static/style.css Normal file
View File

@@ -0,0 +1,508 @@
/* Main Systades Styles - Dark Mystical Theme */
/* Import Fonts */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap');
/* Root Variables */
:root {
/* Light Theme Colors */
--light-bg-primary: #f8fafc;
--light-bg-secondary: #f1f5f9;
--light-text-primary: #1e293b;
--light-text-secondary: #475569;
--light-accent-primary: #7c3aed;
--light-accent-secondary: #8b5cf6;
--light-border: #e2e8f0;
/* Dark Theme Colors */
--dark-bg-primary: #0a0e19;
--dark-bg-secondary: #111827;
--dark-text-primary: #f9fafb;
--dark-text-secondary: #e5e7eb;
--dark-accent-primary: #6d28d9;
--dark-accent-secondary: #8b5cf6;
--dark-border: #1f2937;
/* Common */
--font-sans: 'Inter', system-ui, -apple-system, sans-serif;
--font-mono: 'JetBrains Mono', monospace;
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--shadow-md: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
--shadow-lg: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
/* Transitions */
--transition-fast: 150ms ease-in-out;
--transition-normal: 300ms ease-in-out;
--transition-slow: 500ms ease-in-out;
}
/* Base Elements */
body {
font-family: var(--font-sans);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
transition: background-color var(--transition-normal), color var(--transition-normal);
background-color: transparent !important; /* Ensure background is transparent */
}
/* HTML root element should also be transparent */
html {
background-color: transparent !important;
}
html.dark {
background-color: transparent !important;
}
/* Theme Specific - keep the color but remove background */
body {
color: var(--light-text-primary);
}
body.dark {
color: var(--dark-text-primary);
background-color: transparent;
}
/* Ensure proper contrast in both modes */
body:not(.dark) {
--text-primary: var(--light-text-primary);
--text-secondary: var(--light-text-secondary);
--bg-primary: var(--light-bg-primary);
--bg-secondary: var(--light-bg-secondary);
}
body.dark {
--text-primary: var(--dark-text-primary);
--text-secondary: var(--dark-text-secondary);
--bg-primary: var(--dark-bg-primary);
--bg-secondary: var(--dark-bg-secondary);
}
/* Typography */
h1, h2, h3, h4, h5, h6 {
font-weight: 600;
line-height: 1.2;
}
p {
line-height: 1.6;
}
.gradient-text {
background-clip: text;
-webkit-background-clip: text;
color: transparent;
position: relative;
}
body .gradient-text {
background-image: linear-gradient(135deg, var(--light-accent-primary), var(--light-accent-secondary));
}
body.dark .gradient-text {
background-image: linear-gradient(135deg, var(--dark-accent-primary), var(--dark-accent-secondary));
}
/* Subtle glow for dark mode gradient text */
body.dark .gradient-text::after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
filter: blur(8px);
opacity: 0.3;
background-image: inherit;
z-index: -1;
pointer-events: none;
}
/* Containers and Layout */
.container {
width: 100%;
margin-left: auto;
margin-right: auto;
padding-left: 1rem;
padding-right: 1rem;
}
@media (min-width: 640px) {
.container {
max-width: 640px;
}
}
@media (min-width: 768px) {
.container {
max-width: 768px;
}
}
@media (min-width: 1024px) {
.container {
max-width: 1024px;
}
}
@media (min-width: 1280px) {
.container {
max-width: 1280px;
}
}
/* Glass Morphism */
.glass-morphism {
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
body .glass-navbar-light {
background-color: rgba(255, 255, 255, 0.8);
border-color: rgba(226, 232, 240, 0.5);
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
}
body.dark .glass-navbar-dark {
background-color: rgba(10, 14, 25, 0.8);
border-color: rgba(31, 41, 55, 0.5);
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.3);
}
/* Navigation */
.nav-link {
padding: 0.5rem 0.75rem;
border-radius: 0.5rem;
transition: all var(--transition-normal);
}
body .nav-link {
color: var(--light-text-secondary);
}
body.dark .nav-link {
color: var(--dark-text-secondary);
}
body .nav-link:hover {
color: var(--light-text-primary);
background-color: rgba(241, 245, 249, 0.5);
}
body.dark .nav-link:hover {
color: var(--dark-text-primary);
background-color: rgba(31, 41, 55, 0.5);
}
body .nav-link-light-active {
color: var(--light-accent-primary);
background-color: rgba(139, 92, 246, 0.1);
}
body.dark .nav-link-active {
color: var(--dark-accent-secondary);
background-color: rgba(109, 40, 217, 0.15);
}
/* Buttons */
.btn {
padding: 0.5rem 1rem;
border-radius: 0.5rem;
font-weight: 500;
transition: all var(--transition-normal);
display: inline-flex;
align-items: center;
justify-content: center;
}
body .btn-primary {
background-color: var(--light-accent-primary);
color: white;
}
body.dark .btn-primary {
background-color: var(--dark-accent-primary);
color: white;
}
body .btn-primary:hover {
background-color: var(--light-accent-secondary);
box-shadow: 0 0 15px rgba(124, 58, 237, 0.3);
}
body.dark .btn-primary:hover {
background-color: var(--dark-accent-secondary);
box-shadow: 0 0 15px rgba(109, 40, 217, 0.5);
}
body .btn-secondary {
background-color: transparent;
border: 1px solid var(--light-border);
color: var(--light-text-primary);
}
body.dark .btn-secondary {
background-color: transparent;
border: 1px solid var(--dark-border);
color: var(--dark-text-primary);
}
body .btn-secondary:hover {
background-color: var(--light-bg-secondary);
border-color: var(--light-accent-secondary);
}
body.dark .btn-secondary:hover {
background-color: var(--dark-bg-secondary);
border-color: var(--dark-accent-secondary);
}
/* Cards */
.card {
border-radius: 0.75rem;
overflow: hidden;
transition: all var(--transition-normal);
}
body .card {
background-color: white;
border: 1px solid var(--light-border);
box-shadow: var(--shadow);
}
body.dark .card {
background-color: var(--dark-bg-secondary);
border: 1px solid var(--dark-border);
box-shadow: var(--shadow);
}
body .card:hover {
transform: translateY(-5px);
box-shadow: var(--shadow-md);
}
body.dark .card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.2);
}
/* Form Elements */
.form-input {
width: 100%;
padding: 0.5rem 0.75rem;
border-radius: 0.5rem;
transition: all var(--transition-normal);
}
body .form-input {
background-color: white;
border: 1px solid var(--light-border);
color: var(--light-text-primary);
}
body.dark .form-input {
background-color: var(--dark-bg-secondary);
border: 1px solid var(--dark-border);
color: var(--dark-text-primary);
}
body .form-input:focus {
outline: none;
border-color: var(--light-accent-secondary);
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.15);
}
body.dark .form-input:focus {
outline: none;
border-color: var(--dark-accent-secondary);
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.25);
}
/* Animations */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.animate-fade-in {
animation: fadeIn 0.5s ease-out forwards;
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
.animate-float {
animation: float 5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 0.7; }
50% { opacity: 1; }
}
.animate-pulse {
animation: pulse 3s ease-in-out infinite;
}
/* Utilities */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
.shadow-elevation {
transition: box-shadow var(--transition-normal), transform var(--transition-normal);
}
body .shadow-elevation {
box-shadow: var(--shadow);
}
body.dark .shadow-elevation {
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2);
}
body .shadow-elevation:hover {
box-shadow: var(--shadow-md);
transform: translateY(-2px);
}
body.dark .shadow-elevation:hover {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.4), 0 4px 6px -2px rgba(0, 0, 0, 0.3);
transform: translateY(-2px);
}
/* Tooltips */
.tooltip {
position: relative;
}
.tooltip:hover::before {
content: attr(data-tooltip);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.75rem;
white-space: nowrap;
z-index: 10;
opacity: 0;
animation: fadeIn 0.3s ease-out forwards;
}
body .tooltip:hover::before {
background-color: var(--light-text-primary);
color: white;
}
body.dark .tooltip:hover::before {
background-color: var(--dark-text-primary);
color: var(--dark-bg-primary);
}
/* Mystical elements */
.mystical-border {
position: relative;
overflow: hidden;
}
.mystical-border::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border: 1px solid;
border-radius: inherit;
pointer-events: none;
opacity: 0.3;
transition: opacity var(--transition-normal);
}
body .mystical-border::after {
border-color: var(--light-accent-primary);
}
body.dark .mystical-border::after {
border-color: var(--dark-accent-primary);
}
.mystical-border:hover::after {
opacity: 0.6;
}
/* Responsive Design Helpers */
@media (max-width: 640px) {
.container {
padding-left: 1rem;
padding-right: 1rem;
}
.hero-heading {
font-size: 2rem;
}
.section-heading {
font-size: 1.5rem;
}
}
/* Accessibility */
:focus-visible {
outline: 2px solid;
outline-offset: 2px;
}
body :focus-visible {
outline-color: var(--light-accent-primary);
}
body.dark :focus-visible {
outline-color: var(--dark-accent-primary);
}
/* Scrollbar styling */
::-webkit-scrollbar {
width: 0.5rem;
height: 0.5rem;
}
body ::-webkit-scrollbar-track {
background: var(--light-bg-secondary);
}
body.dark ::-webkit-scrollbar-track {
background: var(--dark-bg-secondary);
}
body ::-webkit-scrollbar-thumb {
background: var(--light-accent-primary);
border-radius: 0.25rem;
}
body.dark ::-webkit-scrollbar-thumb {
background: var(--dark-accent-primary);
border-radius: 0.25rem;
}
body ::-webkit-scrollbar-thumb:hover {
background: var(--light-accent-secondary);
}
body.dark ::-webkit-scrollbar-thumb:hover {
background: var(--dark-accent-secondary);
}

6
static/three.min.js vendored Normal file

File diff suppressed because one or more lines are too long

308
templates/admin.html Normal file
View File

@@ -0,0 +1,308 @@
{% extends "base.html" %}
{% block title %}Admin-Bereich{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8">
<h1 class="text-3xl font-bold mb-8 text-gray-800 dark:text-white">Admin-Bereich</h1>
<!-- Tabs für verschiedene Bereiche -->
<div x-data="{ activeTab: 'users' }" class="mb-8">
<div class="flex space-x-2 mb-6 overflow-x-auto">
<button
@click="activeTab = 'users'"
:class="activeTab === 'users' ? 'bg-primary-600 text-white' : 'bg-white/10 text-gray-700 dark:text-gray-200'"
class="px-4 py-2 rounded-lg font-medium transition-all">
<i class="fas fa-users mr-2"></i> Benutzer
</button>
<button
@click="activeTab = 'nodes'"
:class="activeTab === 'nodes' ? 'bg-primary-600 text-white' : 'bg-white/10 text-gray-700 dark:text-gray-200'"
class="px-4 py-2 rounded-lg font-medium transition-all">
<i class="fas fa-project-diagram mr-2"></i> Mindmap-Knoten
</button>
<button
@click="activeTab = 'thoughts'"
:class="activeTab === 'thoughts' ? 'bg-primary-600 text-white' : 'bg-white/10 text-gray-700 dark:text-gray-200'"
class="px-4 py-2 rounded-lg font-medium transition-all">
<i class="fas fa-lightbulb mr-2"></i> Gedanken
</button>
<button
@click="activeTab = 'stats'"
:class="activeTab === 'stats' ? 'bg-primary-600 text-white' : 'bg-white/10 text-gray-700 dark:text-gray-200'"
class="px-4 py-2 rounded-lg font-medium transition-all">
<i class="fas fa-chart-bar mr-2"></i> Statistiken
</button>
</div>
<!-- Benutzer-Tab -->
<div x-show="activeTab === 'users'" class="glass-morphism rounded-lg p-6">
<div class="flex justify-between items-center mb-6">
<h2 class="text-xl font-bold text-gray-800 dark:text-white">Benutzerverwaltung</h2>
<button class="btn-outline">
<i class="fas fa-plus mr-2"></i> Neuer Benutzer
</button>
</div>
<div class="overflow-x-auto">
<table class="w-full">
<thead>
<tr class="text-left border-b border-gray-200 dark:border-gray-700">
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">ID</th>
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Benutzername</th>
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">E-Mail</th>
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Admin</th>
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Gedanken</th>
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Aktionen</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr class="border-b border-gray-100 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-dark-700/30">
<td class="px-4 py-3 text-gray-700 dark:text-gray-300">{{ user.id }}</td>
<td class="px-4 py-3 text-gray-700 dark:text-gray-300 font-medium">{{ user.username }}</td>
<td class="px-4 py-3 text-gray-700 dark:text-gray-300">{{ user.email }}</td>
<td class="px-4 py-3 text-gray-700 dark:text-gray-300">
{% if user.is_admin %}
<span class="bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300 px-2 py-1 rounded text-xs">Admin</span>
{% else %}
<span class="bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300 px-2 py-1 rounded text-xs">User</span>
{% endif %}
</td>
<td class="px-4 py-3 text-gray-700 dark:text-gray-300">{{ user.thoughts|length }}</td>
<td class="px-4 py-3 flex space-x-2">
<button class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300">
<i class="fas fa-edit"></i>
</button>
<button class="text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300">
<i class="fas fa-trash-alt"></i>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Mindmap-Knoten-Tab -->
<div x-show="activeTab === 'nodes'" class="glass-morphism rounded-lg p-6">
<div class="flex justify-between items-center mb-6">
<h2 class="text-xl font-bold text-gray-800 dark:text-white">Mindmap-Knoten Verwaltung</h2>
<button class="btn-outline">
<i class="fas fa-plus mr-2"></i> Neuer Knoten
</button>
</div>
<div class="overflow-x-auto">
<table class="w-full">
<thead>
<tr class="text-left border-b border-gray-200 dark:border-gray-700">
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">ID</th>
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Name</th>
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Elternknoten</th>
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Gedanken</th>
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Aktionen</th>
</tr>
</thead>
<tbody>
{% for node in nodes %}
<tr class="border-b border-gray-100 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-dark-700/30">
<td class="px-4 py-3 text-gray-700 dark:text-gray-300">{{ node.id }}</td>
<td class="px-4 py-3 text-gray-700 dark:text-gray-300 font-medium">{{ node.name }}</td>
<td class="px-4 py-3 text-gray-700 dark:text-gray-300">
{% if node.parent %}
{{ node.parent.name }}
{% else %}
<span class="text-gray-400 dark:text-gray-500">Wurzelknoten</span>
{% endif %}
</td>
<td class="px-4 py-3 text-gray-700 dark:text-gray-300">{{ node.thoughts|length }}</td>
<td class="px-4 py-3 flex space-x-2">
<button class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300">
<i class="fas fa-edit"></i>
</button>
<button class="text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300">
<i class="fas fa-trash-alt"></i>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Gedanken-Tab -->
<div x-show="activeTab === 'thoughts'" class="glass-morphism rounded-lg p-6">
<div class="flex justify-between items-center mb-6">
<h2 class="text-xl font-bold text-gray-800 dark:text-white">Gedanken-Verwaltung</h2>
<div class="flex space-x-2">
<div class="relative">
<input type="text" placeholder="Suchen..." class="form-input pl-10 pr-4 py-2 rounded-lg bg-white/10 border border-gray-200/20 dark:border-gray-700/20 focus:outline-none focus:ring-2 focus:ring-primary-500 text-gray-700 dark:text-gray-200">
<div class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500">
<i class="fas fa-search"></i>
</div>
</div>
<button class="btn-outline">
<i class="fas fa-filter mr-2"></i> Filter
</button>
</div>
</div>
<div class="overflow-x-auto">
<table class="w-full">
<thead>
<tr class="text-left border-b border-gray-200 dark:border-gray-700">
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">ID</th>
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Titel</th>
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Autor</th>
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Datum</th>
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Bewertung</th>
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Aktionen</th>
</tr>
</thead>
<tbody>
{% for thought in thoughts %}
<tr class="border-b border-gray-100 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-dark-700/30">
<td class="px-4 py-3 text-gray-700 dark:text-gray-300">{{ thought.id }}</td>
<td class="px-4 py-3 text-gray-700 dark:text-gray-300 font-medium">{{ thought.title }}</td>
<td class="px-4 py-3 text-gray-700 dark:text-gray-300">{{ thought.author.username }}</td>
<td class="px-4 py-3 text-gray-700 dark:text-gray-300">{{ thought.timestamp.strftime('%d.%m.%Y') }}</td>
<td class="px-4 py-3 text-gray-700 dark:text-gray-300">
<div class="flex items-center">
<span class="mr-2">{{ "%.1f"|format(thought.average_rating) }}</span>
<div class="flex">
{% for i in range(5) %}
{% if i < thought.average_rating|int %}
<i class="fas fa-star text-yellow-400"></i>
{% elif i < (thought.average_rating|int + 0.5) %}
<i class="fas fa-star-half-alt text-yellow-400"></i>
{% else %}
<i class="far fa-star text-yellow-400"></i>
{% endif %}
{% endfor %}
</div>
</div>
</td>
<td class="px-4 py-3 flex space-x-2">
<button class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300">
<i class="fas fa-eye"></i>
</button>
<button class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300">
<i class="fas fa-edit"></i>
</button>
<button class="text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300">
<i class="fas fa-trash-alt"></i>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Statistiken-Tab -->
<div x-show="activeTab === 'stats'" class="glass-morphism rounded-lg p-6">
<h2 class="text-xl font-bold mb-6 text-gray-800 dark:text-white">Systemstatistiken</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div class="glass-effect p-4 rounded-lg">
<div class="flex items-center mb-2">
<div class="bg-blue-500/20 p-3 rounded-lg mr-3">
<i class="fas fa-users text-blue-500"></i>
</div>
<div>
<h3 class="text-sm font-medium text-gray-600 dark:text-gray-300">Benutzer</h3>
<p class="text-2xl font-bold text-gray-800 dark:text-white">{{ users|length }}</p>
</div>
</div>
</div>
<div class="glass-effect p-4 rounded-lg">
<div class="flex items-center mb-2">
<div class="bg-purple-500/20 p-3 rounded-lg mr-3">
<i class="fas fa-project-diagram text-purple-500"></i>
</div>
<div>
<h3 class="text-sm font-medium text-gray-600 dark:text-gray-300">Knoten</h3>
<p class="text-2xl font-bold text-gray-800 dark:text-white">{{ nodes|length }}</p>
</div>
</div>
</div>
<div class="glass-effect p-4 rounded-lg">
<div class="flex items-center mb-2">
<div class="bg-green-500/20 p-3 rounded-lg mr-3">
<i class="fas fa-lightbulb text-green-500"></i>
</div>
<div>
<h3 class="text-sm font-medium text-gray-600 dark:text-gray-300">Gedanken</h3>
<p class="text-2xl font-bold text-gray-800 dark:text-white">{{ thoughts|length }}</p>
</div>
</div>
</div>
<div class="glass-effect p-4 rounded-lg">
<div class="flex items-center mb-2">
<div class="bg-red-500/20 p-3 rounded-lg mr-3">
<i class="fas fa-comments text-red-500"></i>
</div>
<div>
<h3 class="text-sm font-medium text-gray-600 dark:text-gray-300">Kommentare</h3>
<p class="text-2xl font-bold text-gray-800 dark:text-white">
{% set comment_count = 0 %}
{% for thought in thoughts %}
{% set comment_count = comment_count + thought.comments|length %}
{% endfor %}
{{ comment_count }}
</p>
</div>
</div>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="glass-effect p-4 rounded-lg">
<h3 class="text-lg font-bold mb-4 text-gray-800 dark:text-white">Aktive Benutzer</h3>
<div class="h-64 flex items-center justify-center bg-gray-100/20 dark:bg-dark-700/20 rounded">
<p class="text-gray-500 dark:text-gray-400">Hier würde ein Aktivitätsdiagramm angezeigt werden</p>
</div>
</div>
<div class="glass-effect p-4 rounded-lg">
<h3 class="text-lg font-bold mb-4 text-gray-800 dark:text-white">Gedanken pro Kategorie</h3>
<div class="h-64 flex items-center justify-center bg-gray-100/20 dark:bg-dark-700/20 rounded">
<p class="text-gray-500 dark:text-gray-400">Hier würde eine Verteilungsstatistik angezeigt werden</p>
</div>
</div>
</div>
</div>
</div>
<!-- System-Log (immer sichtbar) -->
<div class="mt-8">
<h2 class="text-xl font-bold mb-4 text-gray-800 dark:text-white">System-Log</h2>
<div class="glass-morphism rounded-lg p-4 h-32 overflow-y-auto font-mono text-sm text-gray-700 dark:text-gray-300">
<div class="text-green-500">[INFO] [{{ now.strftime('%Y-%m-%d %H:%M:%S') }}] System gestartet</div>
<div class="text-blue-500">[INFO] [{{ now.strftime('%Y-%m-%d %H:%M:%S') }}] Admin-Bereich aufgerufen von {{ current_user.username }}</div>
<div class="text-yellow-500">[WARN] [{{ now.strftime('%Y-%m-%d %H:%M:%S') }}] Hohe Serverauslastung erkannt</div>
<div class="text-gray-500">[INFO] [{{ now.strftime('%Y-%m-%d %H:%M:%S') }}] Backup erfolgreich erstellt</div>
<div class="text-red-500">[ERROR] [{{ now.strftime('%Y-%m-%d %H:%M:%S') }}] API-Zugriffsfehler (Timeout) bei externer Anfrage</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
// Admin-spezifische JavaScript-Funktionen
document.addEventListener('DOMContentLoaded', function() {
console.log('Admin-Bereich geladen');
// Beispiel für AJAX-Ladeverhalten von Daten
// Kann später durch echte API-Calls ersetzt werden
});
</script>
{% endblock %}

View File

@@ -0,0 +1,46 @@
{% 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 %}

71
templates/agb.html Normal file
View File

@@ -0,0 +1,71 @@
{% extends "base.html" %}
{% block title %}AGB{% endblock %}
{% block content %}
<div class="max-w-3xl mx-auto">
<div class="card p-6 md:p-8">
<h1 class="text-3xl font-bold mb-6 gradient-text">Allgemeine Geschäftsbedingungen</h1>
<section class="mb-8">
<h2 class="text-xl font-bold mb-4">1. Geltungsbereich</h2>
<p class="mb-4">Diese Allgemeinen Geschäftsbedingungen (nachfolgend "AGB") gelten für die Nutzung der MindMap-Plattform (nachfolgend "Plattform"), die von der MindMap GmbH, Musterstraße 123, 12345 Musterstadt (nachfolgend "Anbieter") betrieben wird.</p>
<p class="mb-4">Mit der Registrierung und/oder Nutzung der Plattform erkennt der Nutzer diese AGB an. Die Nutzung der Plattform ist nur zulässig, wenn der Nutzer diese AGB akzeptiert.</p>
</section>
<section class="mb-8">
<h2 class="text-xl font-bold mb-4">2. Leistungsbeschreibung</h2>
<p class="mb-4">Die Plattform bietet dem Nutzer die Möglichkeit, komplexe Informationen in Form von Mindmaps zu visualisieren, zu organisieren und zu teilen. Der genaue Funktionsumfang ergibt sich aus der jeweiligen Leistungsbeschreibung auf der Website des Anbieters.</p>
<p class="mb-4">Der Anbieter ist berechtigt, die angebotenen Dienste zu ändern, neue Dienste unentgeltlich oder entgeltlich verfügbar zu machen und die Bereitstellung unentgeltlicher Dienste einzustellen. Der Anbieter wird hierbei jeweils auf die berechtigten Interessen des Nutzers Rücksicht nehmen.</p>
</section>
<section class="mb-8">
<h2 class="text-xl font-bold mb-4">3. Registrierung und Nutzerkonto</h2>
<p class="mb-4">Die Nutzung bestimmter Funktionen der Plattform setzt die Registrierung eines Nutzerkontos voraus. Die Registrierung ist nur volljährigen und voll geschäftsfähigen natürlichen Personen erlaubt.</p>
<p class="mb-4">Der Nutzer verpflichtet sich, bei der Registrierung wahrheitsgemäße und vollständige Angaben zu machen und diese Daten stets aktuell zu halten. Es ist nicht gestattet, mehrere Nutzerkonten zu erstellen.</p>
<p class="mb-4">Der Nutzer ist verpflichtet, seine Zugangsdaten geheim zu halten und vor dem Zugriff durch unbefugte Dritte zu schützen. Der Anbieter wird den Nutzer niemals nach seinem Passwort fragen.</p>
</section>
<section class="mb-8">
<h2 class="text-xl font-bold mb-4">4. Nutzungsrechte</h2>
<p class="mb-4">Der Anbieter gewährt dem Nutzer für die Dauer der Vertragslaufzeit ein einfaches, nicht übertragbares Recht zur Nutzung der Plattform im vertraglich vereinbarten Umfang.</p>
<p class="mb-4">Der Nutzer räumt dem Anbieter an den von ihm auf der Plattform eingestellten Inhalten ein einfaches, übertragbares, unterlizenzierbares, räumlich und zeitlich unbeschränktes Nutzungsrecht ein, soweit dies für den Betrieb der Plattform erforderlich ist.</p>
<p class="mb-4">Der Nutzer garantiert, dass er über alle Rechte an den von ihm eingestellten Inhalten verfügt und durch diese keine Rechte Dritter verletzt werden.</p>
</section>
<section class="mb-8">
<h2 class="text-xl font-bold mb-4">5. Pflichten des Nutzers</h2>
<p class="mb-4">Der Nutzer verpflichtet sich, die Plattform nur im Einklang mit diesen AGB und den geltenden Gesetzen zu nutzen. Insbesondere ist es dem Nutzer untersagt:</p>
<ul class="list-disc pl-6 mb-4 space-y-2">
<li>die Plattform für rechtswidrige oder betrügerische Zwecke zu nutzen</li>
<li>rechtswidrige, beleidigende, diskriminierende oder anderweitig anstößige Inhalte zu verbreiten</li>
<li>Schadsoftware, Viren oder andere schädliche Computercodes zu verbreiten</li>
<li>die normale Funktion der Plattform zu stören oder übermäßig zu belasten</li>
<li>auf die Plattform mit automatisierten Mitteln zuzugreifen (wie z.B. Bots, Scraper)</li>
<li>die Plattform zu reverse-engineeren, zu dekompilieren oder zu disassemblieren</li>
</ul>
<p class="mb-4">Der Anbieter behält sich das Recht vor, bei Verstößen gegen diese Pflichten entsprechende Maßnahmen zu ergreifen, einschließlich der Sperrung des Nutzerkontos.</p>
</section>
<section class="mb-8">
<h2 class="text-xl font-bold mb-4">6. Verfügbarkeit und Wartung</h2>
<p class="mb-4">Der Anbieter ist bemüht, eine hohe Verfügbarkeit der Plattform zu gewährleisten, kann jedoch keine unterbrechungsfreie Verfügbarkeit garantieren. Insbesondere können Wartungsarbeiten, Sicherheits- oder Kapazitätsprobleme sowie Ereignisse, die außerhalb des Einflussbereichs des Anbieters liegen, zu vorübergehenden Unterbrechungen führen.</p>
<p class="mb-4">Der Anbieter wird planmäßige Wartungsarbeiten, sofern möglich, vorher ankündigen und zu Zeiten durchführen, in denen die Nutzung der Plattform typischerweise gering ist.</p>
</section>
<section class="mb-8">
<h2 class="text-xl font-bold mb-4">7. Haftung</h2>
<p class="mb-4">Der Anbieter haftet unbeschränkt für Vorsatz und grobe Fahrlässigkeit sowie nach dem Produkthaftungsgesetz. Für leichte Fahrlässigkeit haftet der Anbieter nur bei Verletzung einer wesentlichen Vertragspflicht und der Höhe nach beschränkt auf die bei Vertragsschluss vorhersehbaren und vertragstypischen Schäden. Wesentliche Vertragspflichten sind solche, deren Erfüllung die ordnungsgemäße Durchführung des Vertrags überhaupt erst ermöglicht und auf deren Einhaltung der Nutzer regelmäßig vertrauen darf.</p>
<p class="mb-4">Diese Haftungsbeschränkung gilt nicht für Schäden aus der Verletzung des Lebens, des Körpers oder der Gesundheit.</p>
</section>
<section class="mb-8">
<h2 class="text-xl font-bold mb-4">8. Schlussbestimmungen</h2>
<p class="mb-4">Es gilt das Recht der Bundesrepublik Deutschland unter Ausschluss des UN-Kaufrechts.</p>
<p class="mb-4">Sollten einzelne Bestimmungen dieser AGB unwirksam sein oder werden, bleibt die Wirksamkeit der übrigen Bestimmungen unberührt.</p>
<p class="mb-4">Der Anbieter behält sich vor, diese AGB jederzeit zu ändern. Änderungen werden dem Nutzer rechtzeitig vor ihrem Inkrafttreten mitgeteilt. Die Änderungen gelten als akzeptiert, wenn der Nutzer ihnen nicht innerhalb von vier Wochen nach Erhalt der Mitteilung widerspricht.</p>
<p class="mb-4">Stand: Mai 2023</p>
</section>
</div>
</div>
{% endblock %}

1051
templates/base.html Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,192 @@
{% extends 'base.html' %}
{% block title %}{{ category.title }} - Forum{% endblock %}
{% block extra_css %}
<style>
.thread-item {
transition: all 0.2s ease;
}
.thread-item:hover {
transform: translateX(2px);
}
.thread-pinned {
border-left-width: 4px;
}
</style>
{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8">
<!-- Breadcrumb Navigation -->
<div class="mb-6 flex items-center text-sm">
<a href="{{ url_for('community') }}" class="opacity-75 hover:opacity-100 transition-opacity">
<i class="fas fa-home mr-1"></i> Forum
</a>
<span class="mx-2 opacity-50">/</span>
<span class="font-medium">{{ category.title }}</span>
</div>
<!-- Kategorie-Header -->
<div class="mb-8 flex flex-wrap items-center justify-between gap-4">
<div class="flex items-center">
<!-- Kategorie-Icon -->
<div class="w-12 h-12 rounded-xl mr-4 flex items-center justify-center text-white"
style="background-color: {{ node.color_code or '#6d28d9' }}">
<i class="fas {{ node.icon or 'fa-folder' }} text-2xl"></i>
</div>
<!-- Kategorie-Info -->
<div>
<h1 class="text-2xl font-bold">{{ category.title }}</h1>
<p class="opacity-75">{{ category.description }}</p>
</div>
</div>
<!-- Neues Thema erstellen -->
<a href="{{ url_for('new_post', category_id=category.id) }}"
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
x-bind:class="darkMode
? 'bg-indigo-700 hover:bg-indigo-600 text-white'
: 'bg-indigo-500 hover:bg-indigo-600 text-white'">
<i class="fas fa-plus-circle mr-2"></i>
Neues Thema
</a>
</div>
<!-- Threads anzeigen -->
<div class="mb-8 rounded-xl overflow-hidden"
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200'">
<!-- Header -->
<div class="p-4 border-b" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
<div class="grid grid-cols-12 gap-4">
<div class="col-span-7 font-medium">Thema</div>
<div class="col-span-1 text-center font-medium hidden md:block">Antworten</div>
<div class="col-span-2 text-center font-medium hidden md:block">Autor</div>
<div class="col-span-2 text-center font-medium hidden md:block">Letzte Antwort</div>
</div>
</div>
<!-- Thread-Liste -->
{% if threads_data %}
{% for thread_data in threads_data %}
{% set thread = thread_data.thread %}
<div class="thread-item p-4 border-b last:border-b-0 {{ 'thread-pinned' if thread.is_pinned }}"
x-bind:class="darkMode
? 'border-white/10 hover:bg-gray-700/50 {{ 'border-l-yellow-500' if thread.is_pinned }}'
: 'border-gray-200 hover:bg-gray-50 {{ 'border-l-yellow-500' if thread.is_pinned }}'">
<a href="{{ url_for('forum_post', post_id=thread.id) }}" class="block">
<div class="grid grid-cols-12 gap-4">
<!-- Thema -->
<div class="col-span-12 md:col-span-7">
<div class="flex items-start">
<!-- Status-Icons -->
<div class="flex flex-col items-center mr-3 pt-1">
{% if thread.is_pinned %}
<i class="fas fa-thumbtack text-yellow-500" title="Angepinnt"></i>
{% endif %}
{% if thread.is_locked %}
<i class="fas fa-lock text-red-500 mt-1" title="Gesperrt"></i>
{% endif %}
</div>
<!-- Themen-Info -->
<div>
<h3 class="font-medium leading-snug mb-1 {% if thread.is_locked %}opacity-70{% endif %}">
{{ thread.title }}
</h3>
<div class="flex items-center text-xs opacity-70 mt-1">
<span><i class="fas fa-eye mr-1"></i> {{ thread.view_count }}</span>
<span class="mx-2 block md:hidden"></span>
<span class="block md:hidden"><i class="fas fa-reply mr-1"></i> {{ thread_data.reply_count }}</span>
<span class="mx-2"></span>
<span><i class="fas fa-clock mr-1"></i> {{ thread.created_at.strftime('%d.%m.%Y, %H:%M') }}</span>
</div>
</div>
</div>
</div>
<!-- Antworten -->
<div class="col-span-1 text-center hidden md:flex items-center justify-center">
<span class="px-2.5 py-1 rounded-full text-sm font-medium"
x-bind:class="darkMode
? 'bg-indigo-900/40 text-indigo-300'
: 'bg-indigo-100 text-indigo-800'">
{{ thread_data.reply_count }}
</span>
</div>
<!-- Autor -->
<div class="col-span-2 text-center hidden md:flex items-center justify-center">
<div class="flex items-center">
<div class="w-7 h-7 rounded-full flex items-center justify-center text-white text-xs font-medium overflow-hidden mr-2"
style="background: linear-gradient(135deg, #8b5cf6, #6366f1);">
{% if thread.author.avatar %}
<img src="{{ thread.author.avatar }}" alt="{{ thread.author.username }}" class="w-full h-full object-cover">
{% else %}
{{ thread.author.username[0].upper() }}
{% endif %}
</div>
<span class="text-sm truncate max-w-[80px]">{{ thread.author.username }}</span>
</div>
</div>
<!-- Letzte Antwort -->
<div class="col-span-2 text-center hidden md:block text-sm">
{% if thread_data.latest_reply %}
<div>{{ thread_data.latest_reply.created_at.strftime('%d.%m.%Y') }}</div>
<div class="opacity-75 text-xs">{{ thread_data.latest_reply.created_at.strftime('%H:%M') }} Uhr</div>
{% else %}
<span class="opacity-60">Keine Antworten</span>
{% endif %}
</div>
</div>
</a>
</div>
{% endfor %}
{% else %}
<div class="p-8 text-center">
<div class="text-3xl mb-3 opacity-30"><i class="fas fa-comments"></i></div>
<h3 class="text-xl font-semibold mb-2">Keine Themen vorhanden</h3>
<p class="opacity-75 mb-4">In dieser Kategorie wurden noch keine Themen erstellt.</p>
<a href="{{ url_for('new_post', category_id=category.id) }}"
class="inline-block px-5 py-2.5 rounded-lg transition-all duration-300"
x-bind:class="darkMode
? 'bg-indigo-700 hover:bg-indigo-600 text-white'
: 'bg-indigo-500 hover:bg-indigo-600 text-white'">
<i class="fas fa-plus-circle mr-2"></i>
Erstes Thema erstellen
</a>
</div>
{% endif %}
</div>
<!-- Link zur Mindmap -->
<div class="rounded-xl p-5 mb-4 flex items-center"
x-bind:class="darkMode ? 'bg-purple-900/20 border border-purple-800/30' : 'bg-purple-50 border border-purple-100'">
<div class="text-3xl mr-4 opacity-80">
<i class="fas fa-diagram-project" style="color: {{ node.color_code }}"></i>
</div>
<div>
<h3 class="font-medium mb-1">Mindmap-Knotenpunkt: {{ node.name }}</h3>
<p class="text-sm opacity-75">In der Mindmap findest du weitere Informationen zu diesem Themenbereich.</p>
</div>
<div class="ml-auto">
<a href="{{ url_for('mindmap') }}"
class="px-4 py-2 rounded-lg inline-block text-sm transition-all"
x-bind:class="darkMode
? 'bg-purple-800/60 hover:bg-purple-700/60 text-white'
: 'bg-white hover:bg-purple-100 text-purple-800 border border-purple-200'">
Zur Mindmap
</a>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// Hier können bei Bedarf kategoriespezifische Scripts eingefügt werden
</script>
{% endblock %}

View File

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

View File

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

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