chore: automatic commit 2025-04-30 12:48

This commit is contained in:
2025-04-30 12:48:06 +02:00
parent f69356473b
commit e4ab1e1bb5
5284 changed files with 868438 additions and 0 deletions

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

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

@@ -0,0 +1,203 @@
/* ChatGPT Assistent Styles - Verbesserte Version */
#chatgpt-assistant {
font-family: 'Inter', sans-serif;
}
#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);
}
#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 {
scrollbar-width: thin;
scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
}
#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;
background-color: #888;
border-radius: 50%;
display: inline-block;
margin: 0 2px;
opacity: 0.4;
animation: bounce 1.4s infinite ease-in-out;
}
.typing-indicator span:nth-child(1) { animation-delay: 0s; }
.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
@keyframes bounce {
0%, 80%, 100% { transform: translateY(0); }
40% { transform: translateY(-8px); }
}
/* Chat Input Fokus-Effekt */
#assistant-chat input:focus {
border-color: var(--primary-500, #3B82F6);
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.25);
}
.dark #assistant-chat input:focus {
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.4);
}
/* Verbesserte Responsive Layouts */
@media (max-width: 640px) {
#assistant-chat {
width: calc(100vw - 2rem) !important;
}
#chatgpt-assistant {
right: 1rem;
bottom: 1rem;
}
}
/* Footer immer unten */
html, body {
height: 100%;
margin: 0;
min-height: 100vh;
}
body {
display: flex;
flex-direction: column;
min-height: 100vh;
}
main {
flex: 1 0 auto;
}
footer {
flex-shrink: 0;
}

526
static/css/base-styles.css Normal file
View File

@@ -0,0 +1,526 @@
/* Base Styles - Dark, Mystical Theme */
/* Global Variables */
:root {
--font-sans: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--font-mono: 'JetBrains Mono', SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
/* Light Theme */
--bg-primary-light: #f8fafc;
--bg-secondary-light: #f1f5f9;
--bg-tertiary-light: #e2e8f0;
--text-primary-light: #1e293b;
--text-secondary-light: #475569;
--accent-primary-light: #7c3aed;
--accent-secondary-light: #8b5cf6;
--accent-tertiary-light: #a78bfa;
--border-light: #e2e8f0;
--shadow-light: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--glow-light: 0 0 15px rgba(139, 92, 246, 0.3);
/* Dark Theme */
--bg-primary-dark: #0a0e19;
--bg-secondary-dark: #111827;
--bg-tertiary-dark: #1f2937;
--text-primary-dark: #f9fafb;
--text-secondary-dark: #e5e7eb;
--accent-primary-dark: #6d28d9;
--accent-secondary-dark: #8b5cf6;
--accent-tertiary-dark: #a78bfa;
--border-dark: #1f2937;
--shadow-dark: 0 4px 6px -1px rgba(0, 0, 0, 0.5), 0 2px 4px -1px rgba(0, 0, 0, 0.3);
--glow-dark: 0 0 15px rgba(124, 58, 237, 0.5);
/* Transitions */
--transition-fast: 150ms ease-in-out;
--transition-normal: 300ms ease-in-out;
--transition-slow: 500ms ease-in-out;
/* Light mode optimierte Farben */
--light-bg: #f9fafb;
--light-text: #1e293b;
--light-heading: #0f172a;
--light-primary: #3b82f6;
--light-primary-hover: #4f46e5;
--light-secondary: #6b7280;
--light-border: #e5e7eb;
--light-card-bg: rgba(255, 255, 255, 0.92);
--light-navbar-bg: rgba(255, 255, 255, 0.92);
--light-input-bg: #ffffff;
--light-input-border: #d1d5db;
--light-input-focus: #3b82f6;
--light-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
/* Base Styles */
html {
font-family: var(--font-sans);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
scroll-behavior: smooth;
}
body {
transition: background-color var(--transition-normal), color var(--transition-normal);
overflow-x: hidden;
min-height: 100vh;
font-family: var(--font-sans);
line-height: 1.5;
}
/* Dark Mode */
html.dark body {
background-color: var(--bg-primary-dark);
color: var(--text-primary-dark);
}
/* Light Mode */
body:not(.dark) {
background-color: var(--light-bg);
color: var(--light-text);
}
/* Typography */
h1, h2, h3, h4, h5, h6 {
font-weight: 600;
line-height: 1.2;
}
.hero-heading {
font-size: 2.75rem;
font-weight: 700;
letter-spacing: -0.02em;
line-height: 1.1;
}
.section-heading {
font-size: 2rem;
font-weight: 700;
letter-spacing: -0.01em;
}
.gradient-text {
background-clip: text;
-webkit-background-clip: text;
color: transparent;
display: inline-block;
}
html.dark .gradient-text {
background-image: linear-gradient(135deg, var(--accent-primary-dark), var(--accent-secondary-dark));
text-shadow: var(--glow-dark);
}
.gradient-text {
background-image: linear-gradient(135deg, var(--accent-primary-light), var(--accent-secondary-light));
text-shadow: var(--glow-light);
}
/* Mystical elements */
.mystical-border {
position: relative;
}
.mystical-border::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border: 1px solid;
border-radius: inherit;
pointer-events: none;
opacity: 0.3;
}
html.dark .mystical-border::before {
border-color: var(--accent-primary-dark);
box-shadow: var(--glow-dark);
}
.mystical-border::before {
border-color: var(--accent-primary-light);
box-shadow: var(--glow-light);
}
/* Navigation Links */
.nav-link {
position: relative;
padding: 0.5rem 0.75rem;
border-radius: 0.5rem;
transition: var(--transition-normal);
}
html.dark .nav-link {
color: var(--text-secondary-dark);
}
.nav-link {
color: var(--text-secondary-light);
}
html.dark .nav-link:hover {
color: var(--text-primary-dark);
background-color: rgba(31, 41, 55, 0.5);
}
.nav-link:hover {
color: var(--text-primary-light);
background-color: rgba(241, 245, 249, 0.5);
}
html.dark .nav-link-active {
color: var(--accent-tertiary-dark);
background-color: rgba(109, 40, 217, 0.15);
}
.nav-link-light-active {
color: var(--accent-primary-light);
background-color: rgba(139, 92, 246, 0.1);
}
/* Glass Morphism Effects */
.glass-navbar-dark {
background-color: rgba(10, 14, 25, 0.8);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-color: rgba(255, 255, 255, 0.1);
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.3);
}
.glass-navbar-light {
background-color: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-color: rgba(226, 232, 240, 0.5);
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
}
.glass-morphism {
transition: background-color var(--transition-normal), backdrop-filter var(--transition-normal);
}
/* Cards */
.mystical-card {
border-radius: 0.75rem;
overflow: hidden;
transition: var(--transition-normal);
}
html.dark .mystical-card {
background-color: var(--bg-secondary-dark);
border: 1px solid var(--border-dark);
box-shadow: var(--shadow-dark);
}
.mystical-card {
background-color: var(--bg-secondary-light);
border: 1px solid var(--border-light);
box-shadow: var(--shadow-light);
}
html.dark .mystical-card:hover {
box-shadow: var(--glow-dark), var(--shadow-dark);
border-color: var(--accent-primary-dark);
}
.mystical-card:hover {
box-shadow: var(--glow-light), var(--shadow-light);
border-color: var(--accent-primary-light);
}
/* Buttons */
.mystical-button {
padding: 0.625rem 1.25rem;
border-radius: 0.5rem;
font-weight: 500;
transition: var(--transition-normal);
position: relative;
overflow: hidden;
}
.mystical-button::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, transparent, rgba(255, 255, 255, 0.05), transparent);
transform: translateX(-100%);
transition: transform 0.8s ease-in-out;
}
.mystical-button:hover::before {
transform: translateX(100%);
}
html.dark .mystical-button-primary {
background-color: var(--accent-primary-dark);
color: white;
}
.mystical-button-primary {
background-color: var(--accent-primary-light);
color: white;
}
html.dark .mystical-button-primary:hover {
background-color: var(--accent-secondary-dark);
box-shadow: var(--glow-dark);
}
.mystical-button-primary:hover {
background-color: var(--accent-secondary-light);
box-shadow: var(--glow-light);
}
html.dark .mystical-button-secondary {
background-color: var(--bg-tertiary-dark);
color: var(--text-primary-dark);
border: 1px solid var(--border-dark);
}
.mystical-button-secondary {
background-color: var(--bg-tertiary-light);
color: var(--text-primary-light);
border: 1px solid var(--border-light);
}
html.dark .mystical-button-secondary:hover {
background-color: var(--bg-secondary-dark);
border-color: var(--accent-tertiary-dark);
}
.mystical-button-secondary:hover {
background-color: var(--bg-secondary-light);
border-color: var(--accent-tertiary-light);
}
/* Inputs */
.mystical-input {
padding: 0.5rem 0.75rem;
border-radius: 0.5rem;
transition: var(--transition-normal);
width: 100%;
outline: none;
}
html.dark .mystical-input {
background-color: var(--bg-tertiary-dark);
border: 1px solid var(--border-dark);
color: var(--text-primary-dark);
}
.mystical-input {
background-color: var(--bg-tertiary-light);
border: 1px solid var(--border-light);
color: var(--text-primary-light);
}
html.dark .mystical-input:focus {
border-color: var(--accent-tertiary-dark);
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.25);
}
.mystical-input:focus {
border-color: var(--accent-tertiary-light);
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.15);
}
/* Animations */
@keyframes floatAnimation {
0% { transform: translateY(0); }
50% { transform: translateY(-5px); }
100% { transform: translateY(0); }
}
.animate-float {
animation: floatAnimation 3s ease-in-out infinite;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.animate-fade-in {
animation: fadeIn 0.5s ease-out forwards;
}
/* Scroll Bars */
::-webkit-scrollbar {
width: 0.5rem;
height: 0.5rem;
}
html.dark ::-webkit-scrollbar-track {
background: var(--bg-secondary-dark);
}
::-webkit-scrollbar-track {
background: var(--bg-secondary-light);
}
html.dark ::-webkit-scrollbar-thumb {
background: var(--accent-primary-dark);
border-radius: 0.25rem;
}
::-webkit-scrollbar-thumb {
background: var(--accent-primary-light);
border-radius: 0.25rem;
}
html.dark ::-webkit-scrollbar-thumb:hover {
background: var(--accent-secondary-dark);
}
::-webkit-scrollbar-thumb:hover {
background: var(--accent-secondary-light);
}
/* Responsive Utilities */
@media (max-width: 640px) {
.hero-heading {
font-size: 2rem;
}
.section-heading {
font-size: 1.5rem;
}
}
/* Additional background elements */
.mystical-dot {
position: absolute;
border-radius: 50%;
opacity: 0.15;
filter: blur(3px);
z-index: -1;
transition: opacity var(--transition-normal);
}
html.dark .mystical-dot {
background-color: var(--accent-primary-dark);
box-shadow: 0 0 15px var(--accent-primary-dark);
}
.mystical-dot {
background-color: var(--accent-primary-light);
box-shadow: 0 0 15px var(--accent-primary-light);
}
/* Accessibility */
.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;
}
/* Focus styles for keyboard navigation */
:focus-visible {
outline: 2px solid var(--accent-primary-light);
outline-offset: 2px;
}
html.dark :focus-visible {
outline-color: var(--accent-primary-dark);
}
/* Light Mode Überschriften */
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: var(--light-heading);
}
/* Light Mode Links */
body:not(.dark) a {
color: var(--light-primary);
}
body:not(.dark) a:hover {
color: var(--light-primary-hover);
}
/* Light Mode Buttons */
body:not(.dark) .btn,
body:not(.dark) button:not(.toggle) {
background-color: var(--light-primary);
color: white;
border: none;
box-shadow: var(--light-shadow);
border-radius: 0.375rem;
padding: 0.5rem 1rem;
transition: all 0.3s ease;
}
body:not(.dark) .btn:hover,
body:not(.dark) button:not(.toggle):hover {
background-color: var(--light-primary-hover);
transform: translateY(-2px);
box-shadow: 0 6px 10px rgba(0, 0, 0, 0.1);
}
/* Light Mode Cards und Panels */
body:not(.dark) .card,
body:not(.dark) .panel {
background-color: var(--light-card-bg);
border: 1px solid var(--light-border);
border-radius: 0.5rem;
box-shadow: var(--light-shadow);
}
/* Light Mode Tabelle */
body:not(.dark) table {
background-color: var(--light-card-bg);
border-collapse: collapse;
}
body:not(.dark) th {
background-color: var(--light-bg);
color: var(--light-heading);
border-bottom: 1px solid var(--light-border);
}
body:not(.dark) td {
border-bottom: 1px solid var(--light-border);
}
/* Light Mode Inputs */
body:not(.dark) input,
body:not(.dark) textarea,
body:not(.dark) select {
background-color: var(--light-input-bg);
border: 1px solid var(--light-input-border);
color: var(--light-text);
border-radius: 0.375rem;
padding: 0.5rem;
}
body:not(.dark) input:focus,
body:not(.dark) textarea:focus,
body:not(.dark) select:focus {
border-color: var(--light-input-focus);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
outline: none;
}
/* Navbar im Light Mode verbessern */
body:not(.dark) nav,
body:not(.dark) .navbar {
background-color: var(--light-navbar-bg);
box-shadow: var(--light-shadow);
border-bottom: 1px solid var(--light-border);
}

3884
static/css/main.css Normal file

File diff suppressed because it is too large Load Diff

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

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

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

@@ -0,0 +1,25 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
from PIL import Image
import cairosvg
# Pfad zum SVG-Favicon
svg_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'favicon.svg')
# Ausgabepfad für das PNG
png_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'favicon.png')
# Ausgabepfad für das ICO
ico_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'favicon.ico')
# SVG zu PNG konvertieren
cairosvg.svg2png(url=svg_path, write_to=png_path, output_width=512, output_height=512)
# PNG zu ICO konvertieren
img = Image.open(png_path)
img.save(ico_path, sizes=[(16, 16), (32, 32), (48, 48), (64, 64), (128, 128)])
print(f"Favicon erfolgreich erstellt: {ico_path}")
# Optional: PNG-Datei löschen, wenn nur ICO benötigt wird
# os.remove(png_path)

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

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

234
static/js/mindmap.html Normal file
View File

@@ -0,0 +1,234 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Interaktive Mindmap</title>
<!-- Cytoscape.js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.26.0/cytoscape.min.js"></script>
<!-- Socket.IO -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.2/socket.io.min.js"></script>
<!-- Feather Icons (optional) -->
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f9fafb;
color: #111827;
line-height: 1.5;
}
.container {
display: flex;
flex-direction: column;
height: 100vh;
width: 100%;
}
.header {
background-color: #1f2937;
color: white;
padding: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 {
font-size: 1.5rem;
font-weight: 500;
}
.toolbar {
background-color: #f3f4f6;
padding: 0.75rem;
display: flex;
gap: 0.5rem;
border-bottom: 1px solid #e5e7eb;
}
.btn {
background-color: #3b82f6;
color: white;
border: none;
border-radius: 0.25rem;
padding: 0.5rem 1rem;
font-size: 0.875rem;
cursor: pointer;
transition: background-color 0.2s;
display: flex;
align-items: center;
gap: 0.5rem;
}
.btn:hover {
background-color: #2563eb;
}
.btn-secondary {
background-color: #6b7280;
}
.btn-secondary:hover {
background-color: #4b5563;
}
.btn-danger {
background-color: #ef4444;
}
.btn-danger:hover {
background-color: #dc2626;
}
.search-container {
flex: 1;
display: flex;
margin-left: 1rem;
}
.search-input {
width: 100%;
max-width: 300px;
padding: 0.5rem;
border: 1px solid #d1d5db;
border-radius: 0.25rem;
font-size: 0.875rem;
}
#cy {
flex: 1;
width: 100%;
position: relative;
}
.category-filters {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
padding: 0.75rem;
background-color: #ffffff;
border-bottom: 1px solid #e5e7eb;
}
.category-filter {
border: none;
border-radius: 0.25rem;
padding: 0.25rem 0.75rem;
font-size: 0.75rem;
cursor: pointer;
transition: opacity 0.2s;
color: white;
}
.category-filter:not(.active) {
opacity: 0.6;
}
.category-filter:hover:not(.active) {
opacity: 0.8;
}
.footer {
background-color: #f3f4f6;
padding: 0.75rem;
text-align: center;
font-size: 0.75rem;
color: #6b7280;
border-top: 1px solid #e5e7eb;
}
/* Kontextmenü Styling */
#context-menu {
position: absolute;
background-color: white;
border: 1px solid #e5e7eb;
border-radius: 0.25rem;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
z-index: 1000;
}
#context-menu .menu-item {
padding: 0.5rem 1rem;
cursor: pointer;
}
#context-menu .menu-item:hover {
background-color: #f3f4f6;
}
</style>
</head>
<body>
<div class="container">
<header class="header">
<h1>Interaktive Mindmap</h1>
<div class="search-container">
<input type="text" id="search-mindmap" class="search-input" placeholder="Suchen...">
</div>
</header>
<div class="toolbar">
<button id="addNode" class="btn">
<i data-feather="plus-circle"></i>
Knoten hinzufügen
</button>
<button id="addEdge" class="btn">
<i data-feather="git-branch"></i>
Verbindung erstellen
</button>
<button id="editNode" class="btn btn-secondary">
<i data-feather="edit-2"></i>
Knoten bearbeiten
</button>
<button id="deleteNode" class="btn btn-danger">
<i data-feather="trash-2"></i>
Knoten löschen
</button>
<button id="deleteEdge" class="btn btn-danger">
<i data-feather="scissors"></i>
Verbindung löschen
</button>
<button id="reLayout" class="btn btn-secondary">
<i data-feather="refresh-cw"></i>
Layout neu anordnen
</button>
<button id="exportMindmap" class="btn btn-secondary">
<i data-feather="download"></i>
Exportieren
</button>
</div>
<div id="category-filters" class="category-filters">
<!-- Wird dynamisch befüllt -->
</div>
<div id="cy"></div>
<footer class="footer">
Mindmap-Anwendung © 2023
</footer>
</div>
<!-- Unsere Mindmap JS -->
<script src="../js/mindmap.js"></script>
<!-- Icons initialisieren -->
<script>
document.addEventListener('DOMContentLoaded', () => {
if (typeof feather !== 'undefined') {
feather.replace();
}
});
</script>
</body>
</html>

749
static/js/mindmap.js Normal file
View File

@@ -0,0 +1,749 @@
/**
* Mindmap.js - Interaktive Mind-Map Implementierung
* - Cytoscape.js für Graph-Rendering
* - Fetch API für REST-Zugriffe
* - Socket.IO für Echtzeit-Synchronisation
*/
(async () => {
/* 1. Initialisierung und Grundkonfiguration */
const cy = cytoscape({
container: document.getElementById('cy'),
style: [
{
selector: 'node',
style: {
'label': 'data(name)',
'text-valign': 'center',
'color': '#fff',
'background-color': 'data(color)',
'width': 45,
'height': 45,
'font-size': 11,
'text-outline-width': 1,
'text-outline-color': '#000',
'text-outline-opacity': 0.5,
'text-wrap': 'wrap',
'text-max-width': 80
}
},
{
selector: 'node[icon]',
style: {
'background-image': function(ele) {
return `static/img/icons/${ele.data('icon')}.svg`;
},
'background-width': '60%',
'background-height': '60%',
'background-position-x': '50%',
'background-position-y': '40%',
'text-margin-y': 10
}
},
{
selector: 'edge',
style: {
'width': 2,
'line-color': '#888',
'target-arrow-shape': 'triangle',
'curve-style': 'bezier',
'target-arrow-color': '#888'
}
},
{
selector: ':selected',
style: {
'border-width': 3,
'border-color': '#f8f32b'
}
}
],
layout: {
name: 'breadthfirst',
directed: true,
padding: 30,
spacingFactor: 1.2
}
});
/* 2. Hilfs-Funktionen für API-Zugriffe */
const get = async endpoint => {
try {
const response = await fetch(endpoint);
if (!response.ok) {
console.error(`API-Fehler: ${endpoint} antwortet mit Status ${response.status}`);
return []; // Leeres Array zurückgeben bei Fehlern
}
return await response.json();
} catch (error) {
console.error(`Fehler beim Abrufen von ${endpoint}:`, error);
return []; // Leeres Array zurückgeben bei Netzwerkfehlern
}
};
const post = async (endpoint, body) => {
try {
const response = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (!response.ok) {
console.error(`API-Fehler: ${endpoint} antwortet mit Status ${response.status}`);
return {}; // Leeres Objekt zurückgeben bei Fehlern
}
return await response.json();
} catch (error) {
console.error(`Fehler beim POST zu ${endpoint}:`, error);
return {}; // Leeres Objekt zurückgeben bei Netzwerkfehlern
}
};
const del = async endpoint => {
try {
const response = await fetch(endpoint, { method: 'DELETE' });
if (!response.ok) {
console.error(`API-Fehler: ${endpoint} antwortet mit Status ${response.status}`);
return {}; // Leeres Objekt zurückgeben bei Fehlern
}
return await response.json();
} catch (error) {
console.error(`Fehler beim DELETE zu ${endpoint}:`, error);
return {}; // Leeres Objekt zurückgeben bei Netzwerkfehlern
}
};
/* 3. Kategorien laden für Style-Informationen */
let categories = await get('/api/categories');
/* 4. Daten laden und Rendering */
const loadMindmap = async () => {
try {
// Nodes und Beziehungen parallel laden
const [nodes, relationships] = await Promise.all([
get('/api/mind_map_nodes'),
get('/api/node_relationships')
]);
// Graph leeren (für Reload-Fälle)
cy.elements().remove();
// Überprüfen, ob nodes ein Array ist, wenn nicht, setze es auf ein leeres Array
const nodesArray = Array.isArray(nodes) ? nodes : [];
// Knoten zum Graph hinzufügen
cy.add(
nodesArray.map(node => {
// Kategorie-Informationen für Styling abrufen
const category = categories.find(c => c.id === node.category_id) || {};
return {
data: {
id: node.id.toString(),
name: node.name,
description: node.description,
color: node.color_code || category.color_code || '#6b7280',
icon: node.icon || category.icon,
category_id: node.category_id
},
position: node.x && node.y ? { x: node.x, y: node.y } : undefined
};
})
);
// Überprüfen, ob relationships ein Array ist, wenn nicht, setze es auf ein leeres Array
const relationshipsArray = Array.isArray(relationships) ? relationships : [];
// Kanten zum Graph hinzufügen
cy.add(
relationshipsArray.map(rel => ({
data: {
id: `${rel.parent_id}_${rel.child_id}`,
source: rel.parent_id.toString(),
target: rel.child_id.toString()
}
}))
);
// Wenn keine Knoten geladen wurden, Fallback-Knoten erstellen
if (nodesArray.length === 0) {
// Mindestens einen Standardknoten hinzufügen
cy.add({
data: {
id: 'fallback-1',
name: 'Mindmap',
description: 'Erstellen Sie hier Ihre eigene Mindmap',
color: '#3b82f6',
icon: 'help-circle'
},
position: { x: 300, y: 200 }
});
// Erfolgsmeldung anzeigen
console.log('Mindmap erfolgreich initialisiert mit Fallback-Knoten');
// Info-Meldung für Benutzer anzeigen
const infoBox = document.createElement('div');
infoBox.classList.add('info-message');
infoBox.style.position = 'absolute';
infoBox.style.top = '50%';
infoBox.style.left = '50%';
infoBox.style.transform = 'translate(-50%, -50%)';
infoBox.style.padding = '15px 20px';
infoBox.style.backgroundColor = 'rgba(59, 130, 246, 0.9)';
infoBox.style.color = 'white';
infoBox.style.borderRadius = '8px';
infoBox.style.zIndex = '5';
infoBox.style.maxWidth = '80%';
infoBox.style.textAlign = 'center';
infoBox.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)';
infoBox.innerHTML = 'Mindmap erfolgreich initialisiert.<br>Verwenden Sie die Werkzeugleiste, um Knoten hinzuzufügen.';
document.getElementById('cy').appendChild(infoBox);
// Meldung nach 5 Sekunden ausblenden
setTimeout(() => {
infoBox.style.opacity = '0';
infoBox.style.transition = 'opacity 0.5s ease';
setTimeout(() => {
if (infoBox.parentNode) {
infoBox.parentNode.removeChild(infoBox);
}
}, 500);
}, 5000);
}
// Layout anwenden wenn keine Positionsdaten vorhanden
const nodesWithoutPosition = cy.nodes().filter(node =>
!node.position() || (node.position().x === 0 && node.position().y === 0)
);
if (nodesWithoutPosition.length > 0) {
cy.layout({
name: 'breadthfirst',
directed: true,
padding: 30,
spacingFactor: 1.2
}).run();
}
// Tooltip-Funktionalität
cy.nodes().unbind('mouseover').bind('mouseover', (event) => {
const node = event.target;
const description = node.data('description');
if (description) {
const tooltip = document.getElementById('node-tooltip') ||
document.createElement('div');
if (!tooltip.id) {
tooltip.id = 'node-tooltip';
tooltip.style.position = 'absolute';
tooltip.style.backgroundColor = '#333';
tooltip.style.color = '#fff';
tooltip.style.padding = '8px';
tooltip.style.borderRadius = '4px';
tooltip.style.maxWidth = '250px';
tooltip.style.zIndex = 10;
tooltip.style.pointerEvents = 'none';
tooltip.style.transition = 'opacity 0.2s';
tooltip.style.boxShadow = '0 2px 10px rgba(0,0,0,0.3)';
document.body.appendChild(tooltip);
}
const renderedPosition = node.renderedPosition();
const containerRect = cy.container().getBoundingClientRect();
tooltip.innerHTML = description;
tooltip.style.left = (containerRect.left + renderedPosition.x + 25) + 'px';
tooltip.style.top = (containerRect.top + renderedPosition.y - 15) + 'px';
tooltip.style.opacity = '1';
}
});
cy.nodes().unbind('mouseout').bind('mouseout', () => {
const tooltip = document.getElementById('node-tooltip');
if (tooltip) {
tooltip.style.opacity = '0';
}
});
} catch (error) {
console.error('Fehler beim Laden der Mindmap:', error);
alert('Die Mindmap konnte nicht geladen werden. Bitte prüfen Sie die Konsole für Details.');
}
};
// Initial laden
await loadMindmap();
/* 5. Socket.IO für Echtzeit-Synchronisation */
const socket = io();
socket.on('node_added', async (node) => {
// Kategorie-Informationen für Styling abrufen
const category = categories.find(c => c.id === node.category_id) || {};
cy.add({
data: {
id: node.id.toString(),
name: node.name,
description: node.description,
color: node.color_code || category.color_code || '#6b7280',
icon: node.icon || category.icon,
category_id: node.category_id
}
});
// Layout neu anwenden, wenn nötig
if (!node.x || !node.y) {
cy.layout({ name: 'breadthfirst', directed: true, padding: 30 }).run();
}
});
socket.on('node_updated', (node) => {
const cyNode = cy.$id(node.id.toString());
if (cyNode.length > 0) {
// Kategorie-Informationen für Styling abrufen
const category = categories.find(c => c.id === node.category_id) || {};
cyNode.data({
name: node.name,
description: node.description,
color: node.color_code || category.color_code || '#6b7280',
icon: node.icon || category.icon,
category_id: node.category_id
});
if (node.x && node.y) {
cyNode.position({ x: node.x, y: node.y });
}
}
});
socket.on('node_deleted', (nodeId) => {
const cyNode = cy.$id(nodeId.toString());
if (cyNode.length > 0) {
cy.remove(cyNode);
}
});
socket.on('relationship_added', (rel) => {
cy.add({
data: {
id: `${rel.parent_id}_${rel.child_id}`,
source: rel.parent_id.toString(),
target: rel.child_id.toString()
}
});
});
socket.on('relationship_deleted', (rel) => {
const edgeId = `${rel.parent_id}_${rel.child_id}`;
const cyEdge = cy.$id(edgeId);
if (cyEdge.length > 0) {
cy.remove(cyEdge);
}
});
socket.on('category_updated', async () => {
// Kategorien neu laden
categories = await get('/api/categories');
// Nodes aktualisieren, die diese Kategorie verwenden
cy.nodes().forEach(node => {
const categoryId = node.data('category_id');
if (categoryId) {
const category = categories.find(c => c.id === categoryId);
if (category) {
node.data('color', node.data('color_code') || category.color_code);
node.data('icon', node.data('icon') || category.icon);
}
}
});
});
/* 6. UI-Interaktionen */
// Knoten hinzufügen
const btnAddNode = document.getElementById('addNode');
if (btnAddNode) {
btnAddNode.addEventListener('click', async () => {
const name = prompt('Knotenname eingeben:');
if (!name) return;
const description = prompt('Beschreibung (optional):');
// Kategorie auswählen
let categoryId = null;
if (categories.length > 0) {
const categoryOptions = categories.map((c, i) => `${i}: ${c.name}`).join('\n');
const categoryChoice = prompt(
`Kategorie auswählen (Nummer eingeben):\n${categoryOptions}`,
'0'
);
if (categoryChoice !== null) {
const index = parseInt(categoryChoice, 10);
if (!isNaN(index) && index >= 0 && index < categories.length) {
categoryId = categories[index].id;
}
}
}
// Knoten erstellen
await post('/api/mind_map_node', {
name,
description,
category_id: categoryId
});
// Darstellung wird durch Socket.IO Event übernommen
});
}
// Verbindung hinzufügen
const btnAddEdge = document.getElementById('addEdge');
if (btnAddEdge) {
btnAddEdge.addEventListener('click', async () => {
const sel = cy.$('node:selected');
if (sel.length !== 2) {
alert('Bitte genau zwei Knoten auswählen (Parent → Child)');
return;
}
const [parent, child] = sel.map(node => node.id());
await post('/api/node_relationship', {
parent_id: parent,
child_id: child
});
// Darstellung wird durch Socket.IO Event übernommen
});
}
// Knoten bearbeiten
const btnEditNode = document.getElementById('editNode');
if (btnEditNode) {
btnEditNode.addEventListener('click', async () => {
const sel = cy.$('node:selected');
if (sel.length !== 1) {
alert('Bitte genau einen Knoten auswählen');
return;
}
const node = sel[0];
const nodeData = node.data();
const name = prompt('Knotenname:', nodeData.name);
if (!name) return;
const description = prompt('Beschreibung:', nodeData.description || '');
// Kategorie auswählen
let categoryId = nodeData.category_id;
if (categories.length > 0) {
const categoryOptions = categories.map((c, i) => `${i}: ${c.name}`).join('\n');
const categoryChoice = prompt(
`Kategorie auswählen (Nummer eingeben):\n${categoryOptions}`,
categories.findIndex(c => c.id === categoryId).toString()
);
if (categoryChoice !== null) {
const index = parseInt(categoryChoice, 10);
if (!isNaN(index) && index >= 0 && index < categories.length) {
categoryId = categories[index].id;
}
}
}
// Knoten aktualisieren
await post(`/api/mind_map_node/${nodeData.id}`, {
name,
description,
category_id: categoryId
});
// Darstellung wird durch Socket.IO Event übernommen
});
}
// Knoten löschen
const btnDeleteNode = document.getElementById('deleteNode');
if (btnDeleteNode) {
btnDeleteNode.addEventListener('click', async () => {
const sel = cy.$('node:selected');
if (sel.length !== 1) {
alert('Bitte genau einen Knoten auswählen');
return;
}
if (confirm('Sind Sie sicher, dass Sie diesen Knoten löschen möchten?')) {
const nodeId = sel[0].id();
await del(`/api/mind_map_node/${nodeId}`);
// Darstellung wird durch Socket.IO Event übernommen
}
});
}
// Verbindung löschen
const btnDeleteEdge = document.getElementById('deleteEdge');
if (btnDeleteEdge) {
btnDeleteEdge.addEventListener('click', async () => {
const sel = cy.$('edge:selected');
if (sel.length !== 1) {
alert('Bitte genau eine Verbindung auswählen');
return;
}
if (confirm('Sind Sie sicher, dass Sie diese Verbindung löschen möchten?')) {
const edge = sel[0];
const parentId = edge.source().id();
const childId = edge.target().id();
await del(`/api/node_relationship/${parentId}/${childId}`);
// Darstellung wird durch Socket.IO Event übernommen
}
});
}
// Layout aktualisieren
const btnReLayout = document.getElementById('reLayout');
if (btnReLayout) {
btnReLayout.addEventListener('click', () => {
cy.layout({
name: 'breadthfirst',
directed: true,
padding: 30,
spacingFactor: 1.2
}).run();
});
}
/* 7. Position speichern bei Drag & Drop */
cy.on('dragfree', 'node', async (e) => {
const node = e.target;
const position = node.position();
await post(`/api/mind_map_node/${node.id()}/position`, {
x: Math.round(position.x),
y: Math.round(position.y)
});
// Andere Benutzer erhalten die Position über den node_updated Event
});
/* 8. Kontextmenü (optional) */
const setupContextMenu = () => {
cy.on('cxttap', 'node', function(e) {
const node = e.target;
const nodeData = node.data();
// Position des Kontextmenüs berechnen
const renderedPosition = node.renderedPosition();
const containerRect = cy.container().getBoundingClientRect();
const menuX = containerRect.left + renderedPosition.x;
const menuY = containerRect.top + renderedPosition.y;
// Kontextmenü erstellen oder aktualisieren
let contextMenu = document.getElementById('context-menu');
if (!contextMenu) {
contextMenu = document.createElement('div');
contextMenu.id = 'context-menu';
contextMenu.style.position = 'absolute';
contextMenu.style.backgroundColor = '#fff';
contextMenu.style.border = '1px solid #ccc';
contextMenu.style.borderRadius = '4px';
contextMenu.style.padding = '5px 0';
contextMenu.style.boxShadow = '0 2px 10px rgba(0,0,0,0.2)';
contextMenu.style.zIndex = 1000;
document.body.appendChild(contextMenu);
}
// Menüinhalte
contextMenu.innerHTML = `
<div class="menu-item" data-action="edit">Knoten bearbeiten</div>
<div class="menu-item" data-action="connect">Verbindung erstellen</div>
<div class="menu-item" data-action="delete">Knoten löschen</div>
`;
// Styling für Menüpunkte
const menuItems = contextMenu.querySelectorAll('.menu-item');
menuItems.forEach(item => {
item.style.padding = '8px 20px';
item.style.cursor = 'pointer';
item.style.fontSize = '14px';
item.addEventListener('mouseover', function() {
this.style.backgroundColor = '#f0f0f0';
});
item.addEventListener('mouseout', function() {
this.style.backgroundColor = 'transparent';
});
// Event-Handler
item.addEventListener('click', async function() {
const action = this.getAttribute('data-action');
switch(action) {
case 'edit':
// Knoten bearbeiten (gleiche Logik wie beim Edit-Button)
const name = prompt('Knotenname:', nodeData.name);
if (name) {
const description = prompt('Beschreibung:', nodeData.description || '');
await post(`/api/mind_map_node/${nodeData.id}`, { name, description });
}
break;
case 'connect':
// Modus zum Verbinden aktivieren
cy.nodes().unselect();
node.select();
alert('Wählen Sie nun einen zweiten Knoten aus, um eine Verbindung zu erstellen');
break;
case 'delete':
if (confirm('Sind Sie sicher, dass Sie diesen Knoten löschen möchten?')) {
await del(`/api/mind_map_node/${nodeData.id}`);
}
break;
}
// Menü schließen
contextMenu.style.display = 'none';
});
});
// Menü positionieren und anzeigen
contextMenu.style.left = menuX + 'px';
contextMenu.style.top = menuY + 'px';
contextMenu.style.display = 'block';
// Event-Listener zum Schließen des Menüs
const closeMenu = function() {
if (contextMenu) {
contextMenu.style.display = 'none';
}
document.removeEventListener('click', closeMenu);
};
// Verzögerung, um den aktuellen Click nicht zu erfassen
setTimeout(() => {
document.addEventListener('click', closeMenu);
}, 0);
e.preventDefault();
});
};
// Kontextmenü aktivieren (optional)
// setupContextMenu();
/* 9. Export-Funktion (optional) */
const btnExport = document.getElementById('exportMindmap');
if (btnExport) {
btnExport.addEventListener('click', () => {
const elements = cy.json().elements;
const exportData = {
nodes: elements.nodes.map(n => ({
id: n.data.id,
name: n.data.name,
description: n.data.description,
category_id: n.data.category_id,
x: Math.round(n.position?.x || 0),
y: Math.round(n.position?.y || 0)
})),
relationships: elements.edges.map(e => ({
parent_id: e.data.source,
child_id: e.data.target
}))
};
const blob = new Blob([JSON.stringify(exportData, null, 2)], {type: 'application/json'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'mindmap_export.json';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
});
}
/* 10. Filter-Funktion nach Kategorien (optional) */
const setupCategoryFilters = () => {
const filterContainer = document.getElementById('category-filters');
if (!filterContainer || !categories.length) return;
filterContainer.innerHTML = '';
// "Alle anzeigen" Option
const allBtn = document.createElement('button');
allBtn.innerText = 'Alle Kategorien';
allBtn.className = 'category-filter active';
allBtn.onclick = () => {
document.querySelectorAll('.category-filter').forEach(btn => btn.classList.remove('active'));
allBtn.classList.add('active');
cy.nodes().removeClass('filtered').show();
cy.edges().show();
};
filterContainer.appendChild(allBtn);
// Filter-Button pro Kategorie
categories.forEach(category => {
const btn = document.createElement('button');
btn.innerText = category.name;
btn.className = 'category-filter';
btn.style.backgroundColor = category.color_code;
btn.style.color = '#fff';
btn.onclick = () => {
document.querySelectorAll('.category-filter').forEach(btn => btn.classList.remove('active'));
btn.classList.add('active');
const matchingNodes = cy.nodes().filter(node => node.data('category_id') === category.id);
cy.nodes().addClass('filtered').hide();
matchingNodes.removeClass('filtered').show();
// Verbindungen zu/von diesen Knoten anzeigen
cy.edges().hide();
matchingNodes.connectedEdges().show();
};
filterContainer.appendChild(btn);
});
};
// Filter-Funktionalität aktivieren (optional)
// setupCategoryFilters();
/* 11. Suchfunktion (optional) */
const searchInput = document.getElementById('search-mindmap');
if (searchInput) {
searchInput.addEventListener('input', (e) => {
const searchTerm = e.target.value.toLowerCase();
if (!searchTerm) {
cy.nodes().removeClass('search-hidden').show();
cy.edges().show();
return;
}
cy.nodes().forEach(node => {
const name = node.data('name').toLowerCase();
const description = (node.data('description') || '').toLowerCase();
if (name.includes(searchTerm) || description.includes(searchTerm)) {
node.removeClass('search-hidden').show();
node.connectedEdges().show();
} else {
node.addClass('search-hidden').hide();
// Kanten nur verstecken, wenn beide verbundenen Knoten versteckt sind
node.connectedEdges().forEach(edge => {
const otherNode = edge.source().id() === node.id() ? edge.target() : edge.source();
if (otherNode.hasClass('search-hidden')) {
edge.hide();
}
});
}
});
});
}
console.log('Mindmap erfolgreich initialisiert');
})();

View File

@@ -0,0 +1,572 @@
/**
* 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'
? 'bg-primary-100 dark:bg-primary-900 text-gray-800 dark:text-white rounded-lg py-2 px-3 max-w-[85%]'
: 'bg-gray-100 dark:bg-dark-700 text-gray-800 dark:text-white rounded-lg py-2 px-3 max-w-[85%]';
// Formatierung des Texts (mit Markdown für Assistent-Nachrichten)
let formattedText = '';
if (sender === 'assistant' && this.markdownParser) {
// Für Assistentnachrichten Markdown verwenden
try {
formattedText = this.markdownParser.parse(text);
// CSS für Markdown-Formatierung hinzufügen
const markdownStyles = `
.markdown-bubble h1, .markdown-bubble h2, .markdown-bubble h3,
.markdown-bubble h4, .markdown-bubble h5, .markdown-bubble h6 {
font-weight: bold;
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
.markdown-bubble h1 { font-size: 1.4rem; }
.markdown-bubble h2 { font-size: 1.3rem; }
.markdown-bubble h3 { font-size: 1.2rem; }
.markdown-bubble h4 { font-size: 1.1rem; }
.markdown-bubble ul, .markdown-bubble ol {
padding-left: 1.5rem;
margin: 0.5rem 0;
}
.markdown-bubble ul { list-style-type: disc; }
.markdown-bubble ol { list-style-type: decimal; }
.markdown-bubble p { margin: 0.5rem 0; }
.markdown-bubble code {
font-family: monospace;
background-color: rgba(0, 0, 0, 0.1);
padding: 1px 4px;
border-radius: 3px;
}
.markdown-bubble pre {
background-color: rgba(0, 0, 0, 0.1);
padding: 0.5rem;
border-radius: 4px;
overflow-x: auto;
margin: 0.5rem 0;
}
.markdown-bubble pre code {
background-color: transparent;
padding: 0;
}
.markdown-bubble blockquote {
border-left: 3px solid rgba(0, 0, 0, 0.2);
padding-left: 0.8rem;
margin: 0.5rem 0;
font-style: italic;
}
.dark .markdown-bubble code {
background-color: rgba(255, 255, 255, 0.1);
}
.dark .markdown-bubble pre {
background-color: rgba(255, 255, 255, 0.1);
}
.dark .markdown-bubble blockquote {
border-left-color: rgba(255, 255, 255, 0.2);
}
`;
// Füge die Styles hinzu, wenn sie noch nicht vorhanden sind
if (!document.querySelector('#markdown-chat-styles')) {
const style = document.createElement('style');
style.id = 'markdown-chat-styles';
style.textContent = markdownStyles;
document.head.appendChild(style);
}
// Klasse für Markdown-Formatierung hinzufügen
bubble.classList.add('markdown-bubble');
} catch (error) {
console.error('Fehler bei der Markdown-Formatierung:', error);
// Fallback zur einfachen Formatierung
formattedText = text.split('\n').map(line => {
if (line.trim() === '') return '<br>';
return `<p>${line}</p>`;
}).join('');
}
} else {
// Für Benutzernachrichten einfache Formatierung
formattedText = text.split('\n').map(line => {
if (line.trim() === '') return '<br>';
return `<p>${line}</p>`;
}).join('');
}
bubble.innerHTML = formattedText;
messageEl.appendChild(bubble);
if (this.chatHistory) {
this.chatHistory.appendChild(messageEl);
// Scroll zum Ende des Verlaufs
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
}
}
/**
* Zeigt Vorschläge als klickbare Pills an
* @param {string[]} suggestions - Liste von Vorschlägen
*/
showSuggestions(suggestions) {
if (!this.suggestionArea) return;
// Vorherige Vorschläge entfernen
this.suggestionArea.innerHTML = '';
if (suggestions && suggestions.length > 0) {
suggestions.forEach(suggestion => {
const pill = document.createElement('button');
pill.className = 'suggestion-pill text-sm bg-gray-200 dark:bg-dark-600 hover:bg-gray-300 dark:hover:bg-dark-500 text-gray-800 dark:text-gray-200 rounded-full px-3 py-1 mb-2 transition-colors';
pill.textContent = suggestion;
this.suggestionArea.appendChild(pill);
});
this.suggestionArea.classList.remove('hidden');
} else {
this.suggestionArea.classList.add('hidden');
}
}
/**
* Sendet die Benutzernachricht an den Server und zeigt die Antwort an
*/
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
});
// Ladeindikator entfernen
this.removeLoadingIndicator();
if (!response.ok) {
throw new Error(`Serverfehler: ${response.status} ${response.statusText}`);
}
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();
// Fehlermeldung anzeigen oder Wiederholungsversuch starten
if (this.retryCount < this.maxRetries) {
this.retryCount++;
this.addMessage('assistant', 'Es gab ein Problem mit der Anfrage. Ich versuche es erneut...');
// Kurze Verzögerung vor dem erneuten Versuch
setTimeout(() => {
// Letzte Benutzernachricht aus dem Messages-Array entfernen
const lastUserMessage = this.messages[this.messages.length - 2].content;
this.messages = this.messages.slice(0, -2); // Entferne Benutzernachricht und Fehlermeldung
// Erneuter Versand mit gleicher Nachricht
this.inputField.value = lastUserMessage;
this.sendMessage();
}, 1500);
} else {
// Maximale Anzahl an Wiederholungsversuchen erreicht
this.addMessage('assistant', 'Es tut mir leid, aber es gab ein Problem bei der Verarbeitung deiner Anfrage. Bitte versuche es später noch einmal.');
this.retryCount = 0; // Zurücksetzen für die nächste Anfrage
}
} finally {
this.isLoading = false;
}
}
/**
* Generiert kontextbasierte Vorschläge basierend auf dem aktuellen Chat-Verlauf
*/
generateContextualSuggestions() {
// Basierend auf letzter Antwort des Assistenten, verschiedene Vorschläge generieren
const lastAssistantMessage = this.messages.findLast(msg => msg.role === 'assistant')?.content || '';
let suggestions = [];
// Intelligente Vorschläge basierend auf Kontext
if (lastAssistantMessage.includes('Künstliche Intelligenz') ||
lastAssistantMessage.includes('KI ') ||
lastAssistantMessage.includes('AI ')) {
suggestions = [
"Wie wird KI in der Wissenschaft eingesetzt?",
"Zeige mir Gedanken zum maschinellen Lernen",
"Was ist der Unterschied zwischen KI und ML?"
];
} else if (lastAssistantMessage.includes('Kategorie') ||
lastAssistantMessage.includes('Kategorien')) {
suggestions = [
"Zeige mir die Unterkategorien",
"Welche Gedanken gehören zu dieser Kategorie?",
"Liste alle Wissenschaftskategorien auf"
];
} else if (lastAssistantMessage.includes('Mindmap') ||
lastAssistantMessage.includes('Visualisierung')) {
suggestions = [
"Wie kann ich eine eigene Mindmap erstellen?",
"Zeige mir Beispiele für Mindmaps",
"Wie funktionieren die Verbindungen in Mindmaps?"
];
} else {
// Standardvorschläge
suggestions = [
"Erzähle mir mehr dazu",
"Gibt es Beispiele dafür?",
"Wie kann ich diese Information nutzen?"
];
}
this.showSuggestions(suggestions);
}
/**
* Zeigt einen Ladeindikator im Chat an
*/
showLoadingIndicator() {
if (!this.chatHistory) return;
// Entferne vorhandenen Ladeindikator (falls vorhanden)
this.removeLoadingIndicator();
const loadingEl = document.createElement('div');
loadingEl.id = 'assistant-loading';
loadingEl.className = 'flex justify-start';
const bubble = document.createElement('div');
bubble.className = 'bg-gray-100 dark:bg-dark-700 text-gray-800 dark:text-white rounded-lg py-2 px-3';
bubble.innerHTML = '<div class="typing-indicator"><span></span><span></span><span></span></div>';
loadingEl.appendChild(bubble);
this.chatHistory.appendChild(loadingEl);
// Scroll zum Ende des Verlaufs
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
}
/**
* Entfernt den Ladeindikator aus dem Chat
*/
removeLoadingIndicator() {
const loadingIndicator = document.getElementById('assistant-loading');
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,777 @@
/**
* 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
if (window.MindMap) {
window.MindMap.pageInitializers = window.MindMap.pageInitializers || {};
window.MindMap.pageInitializers.mindmap = initMindmapPage;
}
// Initialisiere die Mindmap-Seite nur, wenn alle Abhängigkeiten vorhanden sind
if (window.MindMap && typeof MindMapVisualization !== 'undefined') {
if (document.body && document.body.dataset && document.body.dataset.page === 'mindmap') {
window.MindMap.pageInitializers = window.MindMap.pageInitializers || {};
window.MindMap.pageInitializers.mindmap = initMindmapPage;
initMindmapPage();
}
}
document.addEventListener('DOMContentLoaded', function() {
// Prüfe, ob wir auf der Mindmap-Seite sind und initialisiere
if (document.body && document.body.dataset && document.body.dataset.page === 'mindmap') {
initMindmapPage();
}
});
/**
* Initialisiert die Mindmap-Seite
*/
function initMindmapPage() {
console.log('Mindmap-Seite Initialisierung startet...');
console.log('D3 Bibliothek verfügbar:', typeof d3 !== 'undefined');
console.log('MindMapVisualization verfügbar:', typeof MindMapVisualization !== 'undefined');
const mindmapContainer = document.getElementById('mindmap-container');
const thoughtsContainer = document.getElementById('thoughts-container');
if (!mindmapContainer) {
console.error('Mindmap-Container nicht gefunden!');
return;
}
console.log('Mindmap-Container gefunden:', mindmapContainer);
// Prüfe, ob D3.js geladen ist
if (typeof d3 === 'undefined') {
console.error('D3.js ist nicht geladen!');
mindmapContainer.innerHTML = `
<div class="glass-effect p-6 text-center">
<div class="text-red-500 mb-4">
<i class="fa-solid fa-triangle-exclamation text-4xl"></i>
</div>
<p class="text-xl">D3.js konnte nicht geladen werden. Bitte laden Sie die Seite neu.</p>
</div>
`;
return;
}
// Prüfe, ob MindMapVisualization definiert ist
if (typeof MindMapVisualization === 'undefined') {
console.error('MindMapVisualization-Klasse ist nicht definiert!');
mindmapContainer.innerHTML = `
<div class="glass-effect p-6 text-center">
<div class="text-red-500 mb-4">
<i class="fa-solid fa-triangle-exclamation text-4xl"></i>
</div>
<p class="text-xl">MindMap-Visualisierung konnte nicht geladen werden. Bitte laden Sie die Seite neu.</p>
</div>
`;
return;
}
// Erstelle die Mindmap-Visualisierung
try {
console.log('Versuche, MindMapVisualization zu erstellen...');
const mindmap = new MindMapVisualization('#mindmap-container', {
height: 600,
onNodeClick: handleNodeClick
});
// Globale Referenz für die Zoom-Buttons erstellen
window.mindmapInstance = mindmap;
// Lade die Mindmap-Daten
mindmap.loadData();
console.log('MindMapVisualization erfolgreich erstellt und geladen');
} catch (error) {
console.error('Fehler beim Erstellen der MindMapVisualization:', error);
mindmapContainer.innerHTML = `
<div class="glass-effect p-6 text-center">
<div class="text-red-500 mb-4">
<i class="fa-solid fa-triangle-exclamation text-4xl"></i>
</div>
<p class="text-xl">Fehler beim Erstellen der Mindmap-Visualisierung:</p>
<p class="text-md mt-2">${error.message}</p>
</div>
`;
return;
}
// Suchfunktion für die Mindmap
const searchInput = document.getElementById('mindmap-search');
if (searchInput) {
searchInput.addEventListener('input', function(e) {
mindmap.filterBySearchTerm(e.target.value);
});
}
/**
* Behandelt Klicks auf Mindmap-Knoten
*/
async function handleNodeClick(node) {
if (!thoughtsContainer) return;
// Zeige Lade-Animation
thoughtsContainer.innerHTML = `
<div class="flex justify-center items-center p-12">
<div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-primary-400"></div>
</div>
`;
try {
// Lade Gedanken für den ausgewählten Knoten
const response = await fetch(`/api/nodes/${node.id}/thoughts`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const thoughts = await response.json();
// Gedanken anzeigen
renderThoughts(thoughts, node.name);
} catch (error) {
console.error('Fehler beim Laden der Gedanken:', error);
thoughtsContainer.innerHTML = `
<div class="glass-effect p-6 text-center">
<div class="text-red-500 mb-4">
<i class="fa-solid fa-triangle-exclamation text-4xl"></i>
</div>
<p class="text-xl">Fehler beim Laden der Gedanken.</p>
<p class="text-gray-300">Bitte versuchen Sie es später erneut.</p>
</div>
`;
}
}
/**
* Rendert die Gedanken in den Container
*/
function renderThoughts(thoughts, nodeName) {
// Wenn keine Gedanken vorhanden sind
if (thoughts.length === 0) {
thoughtsContainer.innerHTML = `
<div class="glass-effect p-6 text-center">
<div class="text-blue-400 mb-4">
<i class="fa-solid fa-info-circle text-4xl"></i>
</div>
<p class="text-xl">Keine Gedanken für "${nodeName}" vorhanden.</p>
<button id="add-thought-btn" class="btn-primary mt-4">
<i class="fa-solid fa-plus mr-2"></i> Gedanke hinzufügen
</button>
</div>
`;
// Event-Listener für den Button
document.getElementById('add-thought-btn').addEventListener('click', () => {
openAddThoughtModal(nodeName);
});
return;
}
// Gedanken anzeigen
thoughtsContainer.innerHTML = `
<div class="flex justify-between items-center mb-6">
<h2 class="text-xl font-bold text-white">Gedanken zu "${nodeName}"</h2>
<button id="add-thought-btn" class="btn-primary">
<i class="fa-solid fa-plus mr-2"></i> Neuer Gedanke
</button>
</div>
<div class="grid grid-cols-1 gap-4" id="thoughts-grid"></div>
`;
// Button-Event-Listener
document.getElementById('add-thought-btn').addEventListener('click', () => {
openAddThoughtModal(nodeName);
});
// Gedanken-Karten rendern
const thoughtsGrid = document.getElementById('thoughts-grid');
thoughts.forEach((thought, index) => {
const card = createThoughtCard(thought);
// Animation verzögern für gestaffeltes Erscheinen
setTimeout(() => {
card.classList.add('opacity-100');
card.classList.remove('opacity-0', 'translate-y-4');
}, index * 100);
thoughtsGrid.appendChild(card);
});
}
/**
* Erstellt eine Gedanken-Karte
*/
function createThoughtCard(thought) {
const card = document.createElement('div');
card.className = 'card transition-all duration-300 opacity-0 translate-y-4 transform hover:shadow-lg border-l-4';
card.style.borderLeftColor = thought.color_code || '#4080ff';
// Karten-Inhalt
card.innerHTML = `
<div class="p-4">
<div class="flex justify-between items-start">
<h3 class="text-lg font-bold text-white">${thought.title}</h3>
<div class="text-sm text-gray-400">${thought.timestamp}</div>
</div>
<div class="prose dark:prose-invert mt-2">
<p>${thought.content}</p>
</div>
${thought.keywords ? `
<div class="flex flex-wrap gap-1 mt-3">
${thought.keywords.split(',').map(keyword =>
`<span class="px-2 py-1 text-xs rounded-full bg-secondary-700 text-white">${keyword.trim()}</span>`
).join('')}
</div>
` : ''}
<div class="mt-4 flex justify-between items-center">
<div class="text-sm text-gray-400">
<i class="fa-solid fa-user mr-1"></i> ${thought.author}
</div>
<div class="flex space-x-2">
<button class="text-sm px-2 py-1 rounded hover:bg-white/10 transition-colors"
onclick="showComments(${thought.id})">
<i class="fa-solid fa-comments mr-1"></i> Kommentare
</button>
<button class="text-sm px-2 py-1 rounded hover:bg-white/10 transition-colors"
onclick="showRelations(${thought.id})">
<i class="fa-solid fa-diagram-project mr-1"></i> Beziehungen
</button>
</div>
</div>
</div>
`;
return card;
}
/**
* Öffnet das Modal zum Hinzufügen eines neuen Gedankens
*/
function openAddThoughtModal(nodeName) {
// Node-Information extrahieren
let nodeId, nodeTitle;
if (typeof nodeName === 'string') {
// Wenn nur ein String übergeben wurde
nodeTitle = nodeName;
// Versuche nodeId aus der Mindmap zu finden
const nodeElement = d3.selectAll('.node-group').filter(d => d.name === nodeName);
if (nodeElement.size() > 0) {
nodeId = nodeElement.datum().id;
}
} else if (typeof nodeName === 'object') {
// Wenn ein Node-Objekt übergeben wurde
nodeId = nodeName.id;
nodeTitle = nodeName.name;
} else {
console.error('Ungültiger Node-Parameter', nodeName);
return;
}
// Modal-Struktur erstellen
const modal = document.createElement('div');
modal.className = 'fixed inset-0 z-50 flex items-center justify-center p-4';
modal.innerHTML = `
<div class="absolute inset-0 bg-black/50 backdrop-blur-sm" id="modal-backdrop"></div>
<div class="glass-effect relative rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto z-10 transform transition-all">
<div class="p-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-bold text-white flex items-center">
<span class="w-3 h-3 rounded-full bg-primary-400 mr-2"></span>
Neuer Gedanke zu "${nodeTitle}"
</h3>
<button id="close-modal-btn" class="text-gray-400 hover:text-white transition-colors">
<i class="fa-solid fa-xmark text-xl"></i>
</button>
</div>
<form id="add-thought-form" class="space-y-4">
<input type="hidden" id="node_id" name="node_id" value="${nodeId || ''}">
<div>
<label for="title" class="block text-sm font-medium text-gray-300">Titel</label>
<input type="text" id="title" name="title" required
class="mt-1 block w-full rounded-md bg-dark-700 border border-dark-500 text-white p-2.5 focus:ring-2 focus:ring-primary-500 focus:border-transparent">
</div>
<div>
<label for="content" class="block text-sm font-medium text-gray-300">Inhalt</label>
<textarea id="content" name="content" rows="5" required
class="mt-1 block w-full rounded-md bg-dark-700 border border-dark-500 text-white p-2.5 focus:ring-2 focus:ring-primary-500 focus:border-transparent"></textarea>
</div>
<div>
<label for="keywords" class="block text-sm font-medium text-gray-300">Schlüsselwörter (kommagetrennt)</label>
<input type="text" id="keywords" name="keywords"
class="mt-1 block w-full rounded-md bg-dark-700 border border-dark-500 text-white p-2.5 focus:ring-2 focus:ring-primary-500 focus:border-transparent">
</div>
<div>
<label for="abstract" class="block text-sm font-medium text-gray-300">Zusammenfassung (optional)</label>
<textarea id="abstract" name="abstract" rows="2"
class="mt-1 block w-full rounded-md bg-dark-700 border border-dark-500 text-white p-2.5 focus:ring-2 focus:ring-primary-500 focus:border-transparent"></textarea>
</div>
<div>
<label for="color_code" class="block text-sm font-medium text-gray-300">Farbcode</label>
<div class="flex space-x-2 mt-1">
<input type="color" id="color_code" name="color_code" value="#4080ff"
class="h-10 w-10 rounded bg-dark-700 border border-dark-500">
<select id="predefined_colors"
class="block flex-grow rounded-md bg-dark-700 border border-dark-500 text-white p-2.5">
<option value="#4080ff">Blau</option>
<option value="#a040ff">Lila</option>
<option value="#40bf80">Grün</option>
<option value="#ff4080">Rot</option>
<option value="#ffaa00">Orange</option>
<option value="#00ccff">Türkis</option>
</select>
</div>
</div>
<div class="flex justify-between pt-4">
<div class="flex items-center">
<div class="relative">
<button type="button" id="open-relation-btn" class="btn-outline text-sm pl-3 pr-9">
<i class="fa-solid fa-diagram-project mr-2"></i> Verbindung
<i class="fa-solid fa-chevron-down absolute right-3 top-1/2 transform -translate-y-1/2"></i>
</button>
<div id="relation-menu" class="absolute left-0 mt-2 w-60 rounded-md shadow-lg bg-dark-800 ring-1 ring-black ring-opacity-5 z-10 hidden">
<div class="py-1">
<div class="px-3 py-2 text-xs font-semibold text-gray-400 border-b border-dark-600">BEZIEHUNGSTYPEN</div>
<div class="max-h-48 overflow-y-auto">
<button type="button" class="relation-type-btn w-full text-left px-4 py-2 text-sm text-white hover:bg-dark-600" data-type="supports">
<i class="fa-solid fa-circle-arrow-up text-green-400 mr-2"></i> Stützt
</button>
<button type="button" class="relation-type-btn w-full text-left px-4 py-2 text-sm text-white hover:bg-dark-600" data-type="contradicts">
<i class="fa-solid fa-circle-arrow-down text-red-400 mr-2"></i> Widerspricht
</button>
<button type="button" class="relation-type-btn w-full text-left px-4 py-2 text-sm text-white hover:bg-dark-600" data-type="builds_upon">
<i class="fa-solid fa-arrow-right text-blue-400 mr-2"></i> Baut auf auf
</button>
<button type="button" class="relation-type-btn w-full text-left px-4 py-2 text-sm text-white hover:bg-dark-600" data-type="generalizes">
<i class="fa-solid fa-arrow-up-wide-short text-purple-400 mr-2"></i> Verallgemeinert
</button>
<button type="button" class="relation-type-btn w-full text-left px-4 py-2 text-sm text-white hover:bg-dark-600" data-type="specifies">
<i class="fa-solid fa-arrow-down-wide-short text-yellow-400 mr-2"></i> Spezifiziert
</button>
<button type="button" class="relation-type-btn w-full text-left px-4 py-2 text-sm text-white hover:bg-dark-600" data-type="inspires">
<i class="fa-solid fa-lightbulb text-amber-400 mr-2"></i> Inspiriert
</button>
</div>
</div>
</div>
</div>
<input type="hidden" id="relation_type" name="relation_type" value="">
<input type="hidden" id="relation_target" name="relation_target" value="">
</div>
<div class="flex space-x-3">
<button type="button" id="cancel-btn" class="btn-outline">Abbrechen</button>
<button type="submit" class="btn-primary">
<i class="fa-solid fa-save mr-2"></i> Speichern
</button>
</div>
</div>
</form>
</div>
</div>
`;
document.body.appendChild(modal);
// Focus auf das erste Feld setzen
setTimeout(() => {
modal.querySelector('#title').focus();
}, 100);
// Event-Listener hinzufügen
modal.querySelector('#modal-backdrop').addEventListener('click', closeModal);
modal.querySelector('#close-modal-btn').addEventListener('click', closeModal);
modal.querySelector('#cancel-btn').addEventListener('click', closeModal);
// Farbauswahl-Event-Listener
const colorInput = modal.querySelector('#color_code');
const predefinedColors = modal.querySelector('#predefined_colors');
predefinedColors.addEventListener('change', function() {
colorInput.value = this.value;
});
// Beziehungsmenü-Funktionalität
const relationBtn = modal.querySelector('#open-relation-btn');
const relationMenu = modal.querySelector('#relation-menu');
relationBtn.addEventListener('click', function() {
relationMenu.classList.toggle('hidden');
});
// Klick außerhalb des Menüs schließt es
document.addEventListener('click', function(event) {
if (!relationBtn.contains(event.target) && !relationMenu.contains(event.target)) {
relationMenu.classList.add('hidden');
}
});
// Beziehungstyp-Auswahl
const relationTypeBtns = modal.querySelectorAll('.relation-type-btn');
const relationTypeInput = modal.querySelector('#relation_type');
relationTypeBtns.forEach(btn => {
btn.addEventListener('click', function() {
const relationType = this.dataset.type;
relationTypeInput.value = relationType;
// Sichtbare Anzeige aktualisieren
relationBtn.innerHTML = `
<i class="fa-solid fa-diagram-project mr-2"></i>
${this.innerText.trim()}
<i class="fa-solid fa-chevron-down absolute right-3 top-1/2 transform -translate-y-1/2"></i>
`;
// Menü schließen
relationMenu.classList.add('hidden');
});
});
// Form-Submit-Handler
const form = modal.querySelector('#add-thought-form');
form.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(form);
const thoughtData = {
node_id: formData.get('node_id'),
title: formData.get('title'),
content: formData.get('content'),
keywords: formData.get('keywords'),
abstract: formData.get('abstract'),
color_code: formData.get('color_code'),
relation_type: formData.get('relation_type'),
relation_target: formData.get('relation_target')
};
try {
const response = await fetch('/api/thoughts', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(thoughtData)
});
if (!response.ok) {
throw new Error('Fehler beim Speichern des Gedankens.');
}
// Modal schließen
closeModal();
// Gedanken neu laden
if (nodeId) {
handleNodeClick({ id: nodeId, name: nodeTitle });
}
// Erfolgsbenachrichtigung
if (window.MindMap && window.MindMap.showNotification) {
window.MindMap.showNotification('Gedanke erfolgreich gespeichert.', 'success');
}
} catch (error) {
console.error('Fehler beim Speichern:', error);
if (window.MindMap && window.MindMap.showNotification) {
window.MindMap.showNotification('Fehler beim Speichern des Gedankens.', 'error');
}
}
});
// Modal schließen
function closeModal() {
modal.classList.add('opacity-0');
setTimeout(() => {
modal.remove();
}, 300);
}
}
}
/**
* Füge globale Funktionen für das Mindmap-Objekt hinzu
*/
window.showComments = async function(thoughtId) {
try {
// Lade-Animation erstellen
const modal = createModalWithLoading('Kommentare werden geladen...');
document.body.appendChild(modal);
// Kommentare laden
const response = await fetch(`/api/comments/${thoughtId}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const comments = await response.json();
// Modal mit Kommentaren aktualisieren
updateModalWithComments(modal, comments, thoughtId);
} catch (error) {
console.error('Fehler beim Laden der Kommentare:', error);
if (window.MindMap && window.MindMap.showNotification) {
window.MindMap.showNotification('Fehler beim Laden der Kommentare.', 'error');
} else {
alert('Fehler beim Laden der Kommentare.');
}
}
};
/**
* Zeigt die Beziehungen eines Gedankens an
*/
window.showRelations = async function(thoughtId) {
try {
// Lade-Animation erstellen
const modal = createModalWithLoading('Beziehungen werden geladen...');
document.body.appendChild(modal);
// Beziehungen laden
const response = await fetch(`/api/thoughts/${thoughtId}/relations`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const relations = await response.json();
// Modal mit Beziehungen aktualisieren
updateModalWithRelations(modal, relations, thoughtId);
} catch (error) {
console.error('Fehler beim Laden der Beziehungen:', error);
if (window.MindMap && window.MindMap.showNotification) {
window.MindMap.showNotification('Fehler beim Laden der Beziehungen.', 'error');
} else {
alert('Fehler beim Laden der Beziehungen.');
}
}
};
/**
* Erstellt ein Modal mit Lade-Animation
*/
function createModalWithLoading(loadingText) {
const modal = document.createElement('div');
modal.className = 'fixed inset-0 z-50 flex items-center justify-center p-4';
modal.innerHTML = `
<div class="absolute inset-0 bg-black/50" id="modal-backdrop"></div>
<div class="glass-effect relative rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto z-10">
<div class="p-6 text-center">
<div class="flex justify-center mb-4">
<div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-primary-400"></div>
</div>
<p class="text-lg text-white">${loadingText}</p>
</div>
</div>
`;
// Event-Listener zum Schließen
modal.querySelector('#modal-backdrop').addEventListener('click', () => {
modal.remove();
});
return modal;
}
/**
* Aktualisiert das Modal mit Kommentaren
*/
function updateModalWithComments(modal, comments, thoughtId) {
const modalContent = modal.querySelector('.glass-effect');
modalContent.innerHTML = `
<div class="p-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-bold text-white">Kommentare</h3>
<button id="close-modal-btn" class="text-gray-400 hover:text-white">
<i class="fa-solid fa-xmark text-xl"></i>
</button>
</div>
<div class="comments-list mb-6 space-y-4">
${comments.length === 0 ?
'<div class="text-center text-gray-400 py-4">Keine Kommentare vorhanden.</div>' :
comments.map(comment => `
<div class="glass-effect p-3 rounded">
<div class="flex justify-between items-start">
<div class="font-medium text-white">${comment.author}</div>
<div class="text-xs text-gray-400">${comment.timestamp}</div>
</div>
<p class="mt-2 text-gray-200">${comment.content}</p>
</div>
`).join('')
}
</div>
<form id="comment-form" class="space-y-3">
<div>
<label for="comment-content" class="block text-sm font-medium text-gray-300">Neuer Kommentar</label>
<textarea id="comment-content" name="content" rows="3" required
class="mt-1 block w-full rounded-md bg-dark-700 border-dark-500 text-white"></textarea>
</div>
<div class="flex justify-end pt-2">
<button type="submit" class="btn-primary">
<i class="fa-solid fa-paper-plane mr-2"></i> Senden
</button>
</div>
</form>
</div>
`;
// Event-Listener hinzufügen
modalContent.querySelector('#close-modal-btn').addEventListener('click', () => {
modal.remove();
});
// Kommentar-Formular
const form = modalContent.querySelector('#comment-form');
form.addEventListener('submit', async (e) => {
e.preventDefault();
const content = form.elements.content.value;
try {
const response = await fetch('/api/comments', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
thought_id: thoughtId,
content: content
})
});
if (!response.ok) {
throw new Error('Fehler beim Speichern des Kommentars.');
}
// Modal schließen
modal.remove();
// Erfolgsbenachrichtigung
MindMap.showNotification('Kommentar erfolgreich gespeichert.', 'success');
} catch (error) {
console.error('Fehler beim Speichern des Kommentars:', error);
MindMap.showNotification('Fehler beim Speichern des Kommentars.', 'error');
}
});
}
/**
* Aktualisiert das Modal mit Beziehungen
*/
function updateModalWithRelations(modal, relations, thoughtId) {
const modalContent = modal.querySelector('.glass-effect');
modalContent.innerHTML = `
<div class="p-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-bold text-white">Beziehungen</h3>
<button id="close-modal-btn" class="text-gray-400 hover:text-white">
<i class="fa-solid fa-xmark text-xl"></i>
</button>
</div>
<div class="relations-list mb-6 space-y-4">
${relations.length === 0 ?
'<div class="text-center text-gray-400 py-4">Keine Beziehungen vorhanden.</div>' :
relations.map(relation => `
<div class="glass-effect p-3 rounded">
<div class="flex items-center">
<span class="inline-block px-2 py-1 rounded-full text-xs font-medium bg-primary-600 text-white">
${relation.relation_type}
</span>
<div class="ml-3">
<div class="text-white">Ziel: Gedanke #${relation.target_id}</div>
<div class="text-xs text-gray-400">Erstellt von ${relation.created_by} am ${relation.created_at}</div>
</div>
</div>
</div>
`).join('')
}
</div>
<form id="relation-form" class="space-y-3">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="target_id" class="block text-sm font-medium text-gray-300">Ziel-Gedanke ID</label>
<input type="number" id="target_id" name="target_id" required
class="mt-1 block w-full rounded-md bg-dark-700 border-dark-500 text-white">
</div>
<div>
<label for="relation_type" class="block text-sm font-medium text-gray-300">Beziehungstyp</label>
<select id="relation_type" name="relation_type" required
class="mt-1 block w-full rounded-md bg-dark-700 border-dark-500 text-white">
<option value="SUPPORTS">Stützt</option>
<option value="CONTRADICTS">Widerspricht</option>
<option value="BUILDS_UPON">Baut auf auf</option>
<option value="GENERALIZES">Verallgemeinert</option>
<option value="SPECIFIES">Spezifiziert</option>
<option value="INSPIRES">Inspiriert</option>
</select>
</div>
</div>
<div class="flex justify-end pt-2">
<button type="submit" class="btn-primary">
<i class="fa-solid fa-plus mr-2"></i> Beziehung erstellen
</button>
</div>
</form>
</div>
`;
// Event-Listener hinzufügen
modalContent.querySelector('#close-modal-btn').addEventListener('click', () => {
modal.remove();
});
// Beziehungs-Formular
const form = modalContent.querySelector('#relation-form');
form.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = {
source_id: thoughtId,
target_id: parseInt(form.elements.target_id.value),
relation_type: form.elements.relation_type.value
};
try {
const response = await fetch('/api/relations', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
if (!response.ok) {
throw new Error('Fehler beim Erstellen der Beziehung.');
}
// Modal schließen
modal.remove();
// Erfolgsbenachrichtigung
MindMap.showNotification('Beziehung erfolgreich erstellt.', 'success');
} catch (error) {
console.error('Fehler beim Erstellen der Beziehung:', error);
MindMap.showNotification('Fehler beim Erstellen der Beziehung.', 'error');
}
});
}

View File

@@ -0,0 +1,844 @@
/**
* MindMap D3.js Modul
* Visualisiert die Mindmap mit D3.js
*/
class MindMapVisualization {
constructor(containerSelector, options = {}) {
this.containerSelector = containerSelector;
this.container = d3.select(containerSelector);
this.width = options.width || this.container.node().clientWidth || 800;
this.height = options.height || 600;
this.nodeRadius = options.nodeRadius || 14;
this.selectedNodeRadius = options.selectedNodeRadius || 20;
this.linkDistance = options.linkDistance || 150;
this.chargeStrength = options.chargeStrength || -900;
this.centerForce = options.centerForce || 0.15;
this.onNodeClick = options.onNodeClick || ((node) => console.log('Node clicked:', node));
this.nodes = [];
this.links = [];
this.simulation = null;
this.svg = null;
this.linkElements = null;
this.nodeElements = null;
this.textElements = null;
this.tooltipEnabled = options.tooltipEnabled !== undefined ? options.tooltipEnabled : true;
this.mouseoverNode = null;
this.selectedNode = null;
this.zoomFactor = 1;
this.tooltipDiv = null;
this.isLoading = true;
// Lade die gemerkten Knoten
this.bookmarkedNodes = this.loadBookmarkedNodes();
// Sicherstellen, dass der Container bereit ist
if (this.container.node()) {
this.init();
this.setupDefaultNodes();
// Sofortige Datenladung
window.setTimeout(() => {
this.loadData();
}, 100);
} else {
console.error('Mindmap-Container nicht gefunden:', containerSelector);
}
}
// Standardknoten als Fallback einrichten, falls die API nicht reagiert
setupDefaultNodes() {
// Basis-Mindmap mit Hauptthemen
const defaultNodes = [
{ id: "root", name: "Wissen", description: "Zentrale Wissensbasis", thought_count: 0 },
{ id: "philosophy", name: "Philosophie", description: "Philosophisches Denken", thought_count: 0 },
{ id: "science", name: "Wissenschaft", description: "Wissenschaftliche Erkenntnisse", thought_count: 0 },
{ id: "technology", name: "Technologie", description: "Technologische Entwicklungen", thought_count: 0 },
{ id: "arts", name: "Künste", description: "Künstlerische Ausdrucksformen", thought_count: 0 }
];
const defaultLinks = [
{ source: "root", target: "philosophy" },
{ source: "root", target: "science" },
{ source: "root", target: "technology" },
{ source: "root", target: "arts" }
];
// Als Fallback verwenden, falls die API fehlschlägt
this.defaultNodes = defaultNodes;
this.defaultLinks = defaultLinks;
}
init() {
// SVG erstellen, wenn noch nicht vorhanden
if (!this.svg) {
// Container zuerst leeren
this.container.html('');
this.svg = this.container
.append('svg')
.attr('width', '100%')
.attr('height', this.height)
.attr('viewBox', `0 0 ${this.width} ${this.height}`)
.attr('class', 'mindmap-svg')
.call(
d3.zoom()
.scaleExtent([0.1, 5])
.on('zoom', (event) => {
this.handleZoom(event.transform);
})
);
// Hauptgruppe für alles, was zoom-transformierbar ist
this.g = this.svg.append('g');
// Tooltip initialisieren
if (!d3.select('body').select('.node-tooltip').size()) {
this.tooltipDiv = d3.select('body')
.append('div')
.attr('class', 'node-tooltip')
.style('opacity', 0)
.style('position', 'absolute')
.style('pointer-events', 'none')
.style('background', 'rgba(20, 20, 40, 0.9)')
.style('color', '#ffffff')
.style('border', '1px solid rgba(160, 80, 255, 0.2)')
.style('border-radius', '6px')
.style('padding', '8px 12px')
.style('font-size', '14px')
.style('max-width', '250px')
.style('box-shadow', '0 10px 25px rgba(0, 0, 0, 0.5), 0 0 10px rgba(160, 80, 255, 0.2)');
} else {
this.tooltipDiv = d3.select('body').select('.node-tooltip');
}
}
// Force-Simulation initialisieren
this.simulation = d3.forceSimulation()
.force('link', d3.forceLink().id(d => d.id).distance(this.linkDistance))
.force('charge', d3.forceManyBody().strength(this.chargeStrength))
.force('center', d3.forceCenter(this.width / 2, this.height / 2).strength(this.centerForce))
.force('collision', d3.forceCollide().radius(this.nodeRadius * 2));
// Globale Mindmap-Instanz für externe Zugriffe setzen
window.mindmapInstance = this;
}
handleZoom(transform) {
this.g.attr('transform', transform);
this.zoomFactor = transform.k;
// Knotengröße anpassen, um bei Zoom lesbar zu bleiben
if (this.nodeElements) {
this.nodeElements
.attr('r', d => (d === this.selectedNode ? this.selectedNodeRadius : this.nodeRadius) / Math.sqrt(transform.k));
}
// Textgröße anpassen
if (this.textElements) {
this.textElements
.style('font-size', `${12 / Math.sqrt(transform.k)}px`);
}
}
async loadData() {
try {
// Ladeindikator anzeigen
this.showLoading();
// Verwende sofort die Standarddaten für eine schnelle erste Anzeige
this.nodes = [...this.defaultNodes];
this.links = [...this.defaultLinks];
// Visualisierung sofort aktualisieren
this.isLoading = false;
this.updateVisualization();
// Status auf bereit setzen - don't wait for API
this.container.attr('data-status', 'ready');
// API-Aufruf mit kürzerem Timeout im Hintergrund durchführen
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 Sekunden Timeout
const response = await fetch('/api/mindmap', {
signal: controller.signal,
headers: {
'Cache-Control': 'no-cache',
'Pragma': 'no-cache'
}
});
clearTimeout(timeoutId);
if (!response.ok) {
console.warn(`HTTP Fehler: ${response.status}, versuche erneute Verbindung`);
// Bei Verbindungsfehler versuchen, die Verbindung neu herzustellen
const retryResponse = await fetch('/api/refresh-mindmap', {
headers: {
'Cache-Control': 'no-cache',
'Pragma': 'no-cache'
}
});
if (!retryResponse.ok) {
throw new Error(`Retry failed with status: ${retryResponse.status}`);
}
const retryData = await retryResponse.json();
if (!retryData.success || !retryData.nodes || retryData.nodes.length === 0) {
console.warn('Keine Mindmap-Daten nach Neuversuch, verwende weiterhin Standard-Daten.');
return; // Keep using default data
}
// Flache Liste von Knoten und Verbindungen erstellen
this.nodes = [];
this.links = [];
// Knoten direkt übernehmen
retryData.nodes.forEach(node => {
this.nodes.push({
id: node.id,
name: node.name,
description: node.description || '',
thought_count: node.thought_count || 0,
color: this.generateColorFromString(node.name),
});
// Verbindungen hinzufügen
if (node.connections && node.connections.length > 0) {
node.connections.forEach(conn => {
this.links.push({
source: node.id,
target: conn.target
});
});
}
});
// Visualisierung aktualisieren mit den tatsächlichen Daten
this.updateVisualization();
return;
}
const data = await response.json();
if (!data || !data.nodes || data.nodes.length === 0) {
console.warn('Keine Mindmap-Daten vorhanden, verwende weiterhin Standard-Daten.');
return; // Keep using default data
}
// Flache Liste von Knoten und Verbindungen erstellen
this.nodes = [];
this.links = [];
this.processHierarchicalData(data.nodes);
// Visualisierung aktualisieren mit den tatsächlichen Daten
this.updateVisualization();
} catch (error) {
console.warn('Fehler beim Laden der Mindmap-Daten, verwende Standarddaten:', error);
// Already using default data, no action needed
}
} catch (error) {
console.error('Kritischer Fehler bei der Mindmap-Darstellung:', error);
this.showError('Fehler beim Laden der Mindmap-Daten. Bitte laden Sie die Seite neu.');
this.container.attr('data-status', 'error');
}
}
showLoading() {
// Element nur leeren, wenn es noch kein SVG enthält
if (!this.container.select('svg').size()) {
this.container.html(`
<div class="flex justify-center items-center h-full">
<div class="text-center">
<div class="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-primary-400 mx-auto mb-4"></div>
<p class="text-lg text-white">Mindmap wird geladen...</p>
</div>
</div>
`);
}
}
processHierarchicalData(hierarchicalNodes, parentId = null) {
hierarchicalNodes.forEach(node => {
// Knoten hinzufügen, wenn noch nicht vorhanden
if (!this.nodes.find(n => n.id === node.id)) {
this.nodes.push({
id: node.id,
name: node.name,
description: node.description || '',
thought_count: node.thought_count || 0,
color: this.generateColorFromString(node.name),
});
}
// Verbindung zum Elternknoten hinzufügen
if (parentId !== null) {
this.links.push({
source: parentId,
target: node.id
});
}
// Rekursiv für Kindknoten aufrufen
if (node.children && node.children.length > 0) {
this.processHierarchicalData(node.children, node.id);
}
});
}
generateColorFromString(str) {
// Erzeugt eine deterministische Farbe basierend auf dem String
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
}
// Verwende deterministische Farbe aus unserem Farbschema
const colors = [
'#4080ff', // primary-400
'#a040ff', // secondary-400
'#205cf5', // primary-500
'#8020f5', // secondary-500
'#1040e0', // primary-600
'#6010e0', // secondary-600
];
return colors[Math.abs(hash) % colors.length];
}
updateVisualization() {
// Starte die Visualisierung nur, wenn nicht mehr im Ladezustand
if (this.isLoading) return;
// Container leeren, wenn Diagramm neu erstellt wird
if (!this.svg) {
this.container.html('');
this.init();
}
// Performance-Optimierung: Deaktiviere Transition während des Datenladens
const useTransitions = false;
// Links (Edges) erstellen
this.linkElements = this.g.selectAll('.link')
.data(this.links)
.join(
enter => enter.append('line')
.attr('class', 'link')
.attr('stroke', '#ffffff30')
.attr('stroke-width', 2)
.attr('stroke-dasharray', d => d.relation_type === 'contradicts' ? '5,5' : null)
.attr('marker-end', d => d.relation_type === 'builds_upon' ? 'url(#arrowhead)' : null),
update => update
.attr('stroke', '#ffffff30')
.attr('stroke-dasharray', d => d.relation_type === 'contradicts' ? '5,5' : null)
.attr('marker-end', d => d.relation_type === 'builds_upon' ? 'url(#arrowhead)' : null),
exit => exit.remove()
);
// Pfeilspitze für gerichtete Beziehungen hinzufügen (falls noch nicht vorhanden)
if (!this.svg.select('defs').node()) {
const defs = this.svg.append('defs');
defs.append('marker')
.attr('id', 'arrowhead')
.attr('viewBox', '0 -5 10 10')
.attr('refX', 20)
.attr('refY', 0)
.attr('orient', 'auto')
.attr('markerWidth', 6)
.attr('markerHeight', 6)
.append('path')
.attr('d', 'M0,-5L10,0L0,5')
.attr('fill', '#ffffff50');
}
// Simplified Effekte definieren, falls noch nicht vorhanden
if (!this.svg.select('#glow').node()) {
const defs = this.svg.select('defs').size() ? this.svg.select('defs') : this.svg.append('defs');
// Glow-Effekt für Knoten
const filter = defs.append('filter')
.attr('id', 'glow')
.attr('x', '-50%')
.attr('y', '-50%')
.attr('width', '200%')
.attr('height', '200%');
filter.append('feGaussianBlur')
.attr('stdDeviation', '1')
.attr('result', 'blur');
filter.append('feComposite')
.attr('in', 'SourceGraphic')
.attr('in2', 'blur')
.attr('operator', 'over');
// Blur-Effekt für Schatten
const blurFilter = defs.append('filter')
.attr('id', 'blur')
.attr('x', '-50%')
.attr('y', '-50%')
.attr('width', '200%')
.attr('height', '200%');
blurFilter.append('feGaussianBlur')
.attr('stdDeviation', '1');
}
// Knoten-Gruppe erstellen/aktualisieren
const nodeGroups = this.g.selectAll('.node-group')
.data(this.nodes)
.join(
enter => {
const group = enter.append('g')
.attr('class', 'node-group')
.call(d3.drag()
.on('start', (event, d) => this.dragStarted(event, d))
.on('drag', (event, d) => this.dragged(event, d))
.on('end', (event, d) => this.dragEnded(event, d)));
// Hintergrundschatten für besseren Kontrast
group.append('circle')
.attr('class', 'node-shadow')
.attr('r', d => this.nodeRadius * 1.2)
.attr('fill', 'rgba(0, 0, 0, 0.3)')
.attr('filter', 'url(#blur)');
// Kreis für jeden Knoten
group.append('circle')
.attr('class', d => `node ${this.isNodeBookmarked(d.id) ? 'bookmarked' : ''}`)
.attr('r', this.nodeRadius)
.attr('fill', d => d.color || this.generateColorFromString(d.name))
.attr('stroke', d => this.isNodeBookmarked(d.id) ? '#FFD700' : '#ffffff50')
.attr('stroke-width', d => this.isNodeBookmarked(d.id) ? 3 : 2)
.attr('filter', 'url(#glow)');
// Text-Label mit besserem Kontrast
group.append('text')
.attr('class', 'node-label')
.attr('dy', '0.35em')
.attr('text-anchor', 'middle')
.attr('fill', '#ffffff')
.attr('stroke', 'rgba(0, 0, 0, 0.4)')
.attr('stroke-width', '0.7px')
.attr('paint-order', 'stroke')
.style('font-size', '12px')
.style('font-weight', '500')
.style('pointer-events', 'none')
.text(d => d.name.length > 12 ? d.name.slice(0, 10) + '...' : d.name);
// Interaktivität hinzufügen
group
.on('mouseover', (event, d) => this.nodeMouseover(event, d))
.on('mouseout', (event, d) => this.nodeMouseout(event, d))
.on('click', (event, d) => this.nodeClicked(event, d));
return group;
},
update => {
// Knoten aktualisieren
update.select('.node')
.attr('class', d => `node ${this.isNodeBookmarked(d.id) ? 'bookmarked' : ''}`)
.attr('fill', d => d.color || this.generateColorFromString(d.name))
.attr('stroke', d => this.isNodeBookmarked(d.id) ? '#FFD700' : '#ffffff50')
.attr('stroke-width', d => this.isNodeBookmarked(d.id) ? 3 : 2);
// Text aktualisieren
update.select('.node-label')
.text(d => d.name.length > 12 ? d.name.slice(0, 10) + '...' : d.name);
return update;
},
exit => exit.remove()
);
// Einzelne Elemente für direkten Zugriff speichern
this.nodeElements = this.g.selectAll('.node');
this.textElements = this.g.selectAll('.node-label');
// Performance-Optimierung: Weniger Simulationsschritte für schnellere Stabilisierung
this.simulation
.nodes(this.nodes)
.on('tick', () => this.ticked())
.alpha(0.3) // Reduzierter Wert für schnellere Stabilisierung
.alphaDecay(0.05); // Erhöhter Wert für schnellere Stabilisierung
this.simulation.force('link')
.links(this.links);
// Simulation neu starten
this.simulation.restart();
// Update connection counts
this.updateConnectionCounts();
}
ticked() {
// Linienpositionen aktualisieren
this.linkElements
.attr('x1', d => d.source.x)
.attr('y1', d => d.source.y)
.attr('x2', d => d.target.x)
.attr('y2', d => d.target.y);
// Knotenpositionen aktualisieren
this.g.selectAll('.node-group')
.attr('transform', d => `translate(${d.x}, ${d.y})`);
}
dragStarted(event, d) {
if (!event.active) this.simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
dragEnded(event, d) {
if (!event.active) this.simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
nodeMouseover(event, d) {
this.mouseoverNode = d;
// Tooltip anzeigen
if (this.tooltipEnabled) {
const isBookmarked = this.isNodeBookmarked(d.id);
const tooltipContent = `
<div class="p-2">
<strong>${d.name}</strong>
${d.description ? `<p class="text-sm text-gray-200 mt-1">${d.description}</p>` : ''}
<div class="text-xs text-gray-300 mt-1">
Gedanken: ${d.thought_count}
</div>
<div class="mt-2">
<button id="bookmark-button" class="px-2 py-1 text-xs rounded bg-gray-700 hover:bg-gray-600 text-white"
data-nodeid="${d.id}">
${isBookmarked ? '<i class="fas fa-bookmark mr-1"></i> Gemerkt' : '<i class="far fa-bookmark mr-1"></i> Merken'}
</button>
</div>
</div>
`;
this.tooltipDiv
.html(tooltipContent)
.style('left', (event.pageX + 10) + 'px')
.style('top', (event.pageY - 10) + 'px')
.transition()
.duration(200)
.style('opacity', 1);
// Event-Listener für den Bookmark-Button hinzufügen
document.getElementById('bookmark-button').addEventListener('click', (e) => {
e.stopPropagation();
const nodeId = e.currentTarget.getAttribute('data-nodeid');
const isNowBookmarked = this.toggleBookmark(nodeId);
// Button-Text aktualisieren
if (isNowBookmarked) {
e.currentTarget.innerHTML = '<i class="fas fa-bookmark mr-1"></i> Gemerkt';
} else {
e.currentTarget.innerHTML = '<i class="far fa-bookmark mr-1"></i> Merken';
}
});
}
// Knoten visuell hervorheben
d3.select(event.currentTarget).select('circle')
.transition()
.duration(200)
.attr('r', this.nodeRadius * 1.2)
.attr('stroke', this.isNodeBookmarked(d.id) ? '#FFD700' : '#ffffff');
}
nodeMouseout(event, d) {
this.mouseoverNode = null;
// Tooltip ausblenden
if (this.tooltipEnabled) {
this.tooltipDiv
.transition()
.duration(200)
.style('opacity', 0);
}
// Knoten-Stil zurücksetzen, wenn nicht ausgewählt
const nodeElement = d3.select(event.currentTarget).select('circle');
if (d !== this.selectedNode) {
const isBookmarked = this.isNodeBookmarked(d.id);
nodeElement
.transition()
.duration(200)
.attr('r', this.nodeRadius)
.attr('stroke', isBookmarked ? '#FFD700' : '#ffffff50')
.attr('stroke-width', isBookmarked ? 3 : 2);
}
}
nodeClicked(event, d) {
// Frühere Auswahl zurücksetzen
if (this.selectedNode && this.selectedNode !== d) {
this.g.selectAll('.node')
.filter(n => n === this.selectedNode)
.transition()
.duration(200)
.attr('r', this.nodeRadius)
.attr('stroke', '#ffffff50');
}
// Neue Auswahl hervorheben
if (this.selectedNode !== d) {
this.selectedNode = d;
d3.select(event.currentTarget).select('circle')
.transition()
.duration(200)
.attr('r', this.selectedNodeRadius)
.attr('stroke', '#ffffff');
}
// Callback mit Node-Daten aufrufen
this.onNodeClick(d);
}
showError(message) {
this.container.html(`
<div class="w-full text-center p-6">
<div class="mb-4 text-red-500">
<i class="fas fa-exclamation-triangle text-4xl"></i>
</div>
<p class="text-lg text-gray-200">${message}</p>
</div>
`);
}
// Fokussiert die Ansicht auf einen bestimmten Knoten
focusNode(nodeId) {
const node = this.nodes.find(n => n.id === nodeId);
if (!node) return;
// Simuliere einen Klick auf den Knoten
const nodeElement = this.g.selectAll('.node-group')
.filter(d => d.id === nodeId);
nodeElement.dispatch('click');
// Zentriere den Knoten in der Ansicht
const transform = d3.zoomIdentity
.translate(this.width / 2, this.height / 2)
.scale(1.2)
.translate(-node.x, -node.y);
this.svg.transition()
.duration(750)
.call(
d3.zoom().transform,
transform
);
}
// Filtert die Mindmap basierend auf einem Suchbegriff
filterBySearchTerm(searchTerm) {
if (!searchTerm || searchTerm.trim() === '') {
// Alle Knoten anzeigen
this.g.selectAll('.node-group')
.style('opacity', 1)
.style('pointer-events', 'all');
this.g.selectAll('.link')
.style('opacity', 1);
return;
}
const searchLower = searchTerm.toLowerCase();
const matchingNodes = this.nodes.filter(node =>
node.name.toLowerCase().includes(searchLower) ||
(node.description && node.description.toLowerCase().includes(searchLower))
);
const matchingNodeIds = new Set(matchingNodes.map(n => n.id));
// Passende Knoten hervorheben, andere ausblenden
this.g.selectAll('.node-group')
.style('opacity', d => matchingNodeIds.has(d.id) ? 1 : 0.2)
.style('pointer-events', d => matchingNodeIds.has(d.id) ? 'all' : 'none');
// Verbindungen zwischen passenden Knoten hervorheben
this.g.selectAll('.link')
.style('opacity', d =>
matchingNodeIds.has(d.source.id) && matchingNodeIds.has(d.target.id) ? 1 : 0.1
);
// Auf den ersten passenden Knoten fokussieren, wenn vorhanden
if (matchingNodes.length > 0) {
this.focusNode(matchingNodes[0].id);
}
}
/**
* Updates the thought_count property for each node based on existing connections
*/
updateConnectionCounts() {
// Reset all counts first
this.nodes.forEach(node => {
// Initialize thought_count if it doesn't exist
if (typeof node.thought_count !== 'number') {
node.thought_count = 0;
}
// Count connections for this node
const connectedNodes = this.getConnectedNodes(node);
node.thought_count = connectedNodes.length;
});
// Update UI to show counts
this.updateNodeLabels();
}
/**
* Updates the visual representation of node labels to include connection counts
*/
updateNodeLabels() {
if (!this.textElements) return;
this.textElements.text(d => {
if (d.thought_count > 0) {
return `${d.name} (${d.thought_count})`;
}
return d.name;
});
}
/**
* Adds a new connection between nodes and updates the counts
*/
addConnection(sourceNode, targetNode) {
if (!sourceNode || !targetNode) return false;
// Check if connection already exists
if (this.isConnected(sourceNode, targetNode)) return false;
// Add new connection
this.links.push({
source: sourceNode.id,
target: targetNode.id
});
// Update counts
this.updateConnectionCounts();
// Update visualization
this.updateVisualization();
return true;
}
// Lädt gemerkete Knoten aus dem LocalStorage
loadBookmarkedNodes() {
try {
const bookmarked = localStorage.getItem('bookmarkedNodes');
return bookmarked ? JSON.parse(bookmarked) : [];
} catch (error) {
console.error('Fehler beim Laden der gemerkten Knoten:', error);
return [];
}
}
// Speichert gemerkete Knoten im LocalStorage
saveBookmarkedNodes() {
try {
localStorage.setItem('bookmarkedNodes', JSON.stringify(this.bookmarkedNodes));
} catch (error) {
console.error('Fehler beim Speichern der gemerkten Knoten:', error);
}
}
// Prüft, ob ein Knoten gemerkt ist
isNodeBookmarked(nodeId) {
return this.bookmarkedNodes.includes(nodeId);
}
// Merkt einen Knoten oder hebt die Markierung auf
toggleBookmark(nodeId) {
const index = this.bookmarkedNodes.indexOf(nodeId);
if (index === -1) {
// Node hinzufügen
this.bookmarkedNodes.push(nodeId);
this.updateNodeAppearance(nodeId, true);
} else {
// Node entfernen
this.bookmarkedNodes.splice(index, 1);
this.updateNodeAppearance(nodeId, false);
}
// Änderungen speichern
this.saveBookmarkedNodes();
// Event auslösen für andere Komponenten
const event = new CustomEvent('nodeBookmarkToggled', {
detail: {
nodeId: nodeId,
isBookmarked: index === -1
}
});
document.dispatchEvent(event);
return index === -1; // true wenn jetzt gemerkt, false wenn Markierung aufgehoben
}
// Aktualisiert das Aussehen eines Knotens basierend auf Bookmark-Status
updateNodeAppearance(nodeId, isBookmarked) {
this.g.selectAll('.node-group')
.filter(d => d.id === nodeId)
.select('.node')
.classed('bookmarked', isBookmarked)
.attr('stroke', isBookmarked ? '#FFD700' : '#ffffff50')
.attr('stroke-width', isBookmarked ? 3 : 2);
}
// Aktualisiert das Aussehen aller gemerkten Knoten
updateAllBookmarkedNodes() {
this.g.selectAll('.node-group')
.each((d) => {
const isBookmarked = this.isNodeBookmarked(d.id);
this.updateNodeAppearance(d.id, isBookmarked);
});
}
/**
* Gibt alle direkt verbundenen Knoten eines Knotens zurück
* @param {Object} node - Der Knoten, für den die Verbindungen gesucht werden
* @returns {Array} Array der verbundenen Knotenobjekte
*/
getConnectedNodes(node) {
if (!node || !this.links || !this.nodes) return [];
const nodeId = node.id;
const connectedIds = new Set();
this.links.forEach(link => {
if (link.source === nodeId || (link.source && link.source.id === nodeId)) {
connectedIds.add(link.target.id ? link.target.id : link.target);
}
if (link.target === nodeId || (link.target && link.target.id === nodeId)) {
connectedIds.add(link.source.id ? link.source.id : link.source);
}
});
return this.nodes.filter(n => connectedIds.has(n.id));
}
}
// Exportiere die Klasse für die Verwendung in anderen Modulen
window.MindMapVisualization = MindMapVisualization;

1904
static/mindmap.js Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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